Object.assign(pc, function () {
    'use strict';

    var EVENT_RESIZE = 'resizecanvas';

    var _downsampleImage = function (image, size) {
        var srcW = image.width;
        var srcH = image.height;

        if ((srcW > size) || (srcH > size)) {
            var scale = size / Math.max(srcW, srcH);
            var dstW = Math.floor(srcW * scale);
            var dstH = Math.floor(srcH * scale);

            console.warn('Image dimensions larger than max supported texture size of ' + size + '. ' +
                         'Resizing from ' + srcW + ', ' + srcH + ' to ' + dstW + ', ' + dstH + '.');

            var canvas = document.createElement('canvas');
            canvas.width = dstW;
            canvas.height = dstH;

            var context = canvas.getContext('2d');
            context.drawImage(image, 0, 0, srcW, srcH, 0, 0, dstW, dstH);

            return canvas;
        }

        return image;
    };

    function testRenderable(gl, pixelFormat) {
        var result = true;

        // Create a 2x2 texture
        var texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, pixelFormat, null);

        // Try to use this texture as a render target
        var framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

        // It is legal for a WebGL implementation exposing the OES_texture_float extension to
        // support floating-point textures but not as attachments to framebuffer objects.
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
            result = false;
        }

        // Clean up
        gl.bindTexture(gl.TEXTURE_2D, null);
        gl.deleteTexture(texture);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.deleteFramebuffer(framebuffer);

        return result;
    }

    function testTextureFloatHighPrecision(device) {
        if (!device.textureFloatRenderable)
            return false;

        var chunks = pc.shaderChunks;
        var test1 = chunks.createShaderFromCode(device, chunks.fullscreenQuadVS, chunks.precisionTestPS, "ptest1");
        var test2 = chunks.createShaderFromCode(device, chunks.fullscreenQuadVS, chunks.precisionTest2PS, "ptest2");

        var textureOptions = {
            format: pc.PIXELFORMAT_RGBA32F,
            width: 1,
            height: 1,
            mipmaps: false,
            minFilter: pc.FILTER_NEAREST,
            magFilter: pc.FILTER_NEAREST
        };
        var tex1 = new pc.Texture(device, textureOptions);
        tex1.name = 'testFHP';
        var targ1 = new pc.RenderTarget(device, tex1, {
            depth: false
        });
        pc.drawQuadWithShader(device, targ1, test1);

        textureOptions.format = pc.PIXELFORMAT_R8_G8_B8_A8;
        var tex2 = new pc.Texture(device, textureOptions);
        tex2.name = 'testFHP';
        var targ2 = new pc.RenderTarget(device, tex2, {
            depth: false
        });
        device.constantTexSource.setValue(tex1);
        pc.drawQuadWithShader(device, targ2, test2);

        var prevFramebuffer = device.activeFramebuffer;
        device.setFramebuffer(targ2._glFrameBuffer);

        var pixels = new Uint8Array(4);
        device.readPixels(0, 0, 1, 1, pixels);

        device.setFramebuffer(prevFramebuffer);

        var x = pixels[0] / 255;
        var y = pixels[1] / 255;
        var z = pixels[2] / 255;
        var w = pixels[3] / 255;
        var f = x / (256 * 256 * 256) + y / (256 * 256) + z / 256 + w;

        tex1.destroy();
        targ1.destroy();
        tex2.destroy();
        targ2.destroy();

        return f === 0;
    }

    /**
     * @readonly
     * @name pc.GraphicsDevice#precision
     * @type String
     * @description The highest shader precision supported by this graphics device. Can be 'hiphp', 'mediump' or 'lowp'.
     */
    /**
     * @readonly
     * @name pc.GraphicsDevice#maxCubeMapSize
     * @type Number
     * @description The maximum supported dimension of a cube map.
     */
    /**
     * @readonly
     * @name pc.GraphicsDevice#maxTextureSize
     * @type Number
     * @description The maximum supported dimension of a texture.
     */
    /**
     * @readonly
     * @name pc.GraphicsDevice#maxVolumeSize
     * @type Number
     * @description The maximum supported dimension of a 3D texture (any axis).
     */
    /**
     * @readonly
     * @name pc.GraphicsDevice#maxAnisotropy
     * @type Number
     * @description The maximum supported texture anisotropy setting.
     */
    /**
     * @event
     * @name pc.GraphicsDevice#resizecanvas
     * @description The 'resizecanvas' event is fired when the canvas is resized
     * @param {Number} width The new width of the canvas in pixels
     * @param {Number} height The new height of the canvas in pixels
     */

    /**
     * @constructor
     * @name pc.GraphicsDevice
     * @extends pc.EventHandler
     * @classdesc The graphics device manages the underlying graphics context. It is responsible
     * for submitting render state changes and graphics primitives to the hardware. A graphics
     * device is tied to a specific canvas HTML element. It is valid to have more than one
     * canvas element per page and create a new graphics device against each.
     * @description Creates a new graphics device.
     * @param {HTMLCanvasElement} canvas The canvas to which the graphics device will render.
     * @param {Object} [options] Options passed when creating the WebGL context. More info {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext here}.
     * @property {HTMLCanvasElement} canvas The canvas DOM element that provides the underlying WebGL context used by the graphics device.
     * @property {Boolean} textureFloatRenderable Determines if 32-bit floating-point textures can be used as frame buffer. [read only]
     * @property {Boolean} textureHalfFloatRenderable Determines if 16-bit floating-point textures can be used as frame buffer. [read only]
     * @property {pc.ScopeSpace} scope The scope namespace for shader attributes and variables. [read only]
     */
    var GraphicsDevice = function (canvas, options) {
        pc.EventHandler.call(this);

        var i;
        this.canvas = canvas;
        this.shader = null;
        this.indexBuffer = null;
        this.vertexBuffers = [];
        this.vbOffsets = [];
        this._enableAutoInstancing = false;
        this.autoInstancingMaxObjects = 16384;
        this.attributesInvalidated = true;
        this.boundBuffer = null;
        this.boundElementBuffer = null;
        this.instancedAttribs = { };
        this.enabledAttributes = { };
        this.transformFeedbackBuffer = null;
        this.activeFramebuffer = null;
        this.textureUnit = 0;
        this.textureUnits = [];
        this._maxPixelRatio = 1;
        this.renderTarget = null;
        this.feedback = null;

        // local width/height without pixelRatio applied
        this._width = 0;
        this._height = 0;

        this.updateClientRect();

        // Shader code to WebGL shader cache
        this.vertexShaderCache = {};
        this.fragmentShaderCache = {};

        // Array of WebGL objects that need to be re-initialized after a context restore event
        this.shaders = [];
        this.buffers = [];
        this.textures = [];
        this.targets = [];

        // Add handlers for when the WebGL context is lost or restored
        this.contextLost = false;

        this._contextLostHandler = function (event) {
            event.preventDefault();
            this.contextLost = true;
            // #ifdef DEBUG
            console.log('pc.GraphicsDevice: WebGL context lost.');
            // #endif
            this.fire('devicelost');
        }.bind(this);

        this._contextRestoredHandler = function () {
            // #ifdef DEBUG
            console.log('pc.GraphicsDevice: WebGL context restored.');
            // #endif
            this.initializeContext();
            this.contextLost = false;
            this.fire('devicerestored');
        }.bind(this);

        canvas.addEventListener("webglcontextlost", this._contextLostHandler, false);
        canvas.addEventListener("webglcontextrestored", this._contextRestoredHandler, false);

        // Retrieve the WebGL context
        var preferWebGl2 = (options && options.preferWebGl2 !== undefined) ? options.preferWebGl2 : true;

        var names = preferWebGl2 ? ["webgl2", "experimental-webgl2", "webgl", "experimental-webgl"] :
            ["webgl", "experimental-webgl"];
        var gl = null;
        options = options || {};
        options.stencil = true;
        for (i = 0; i < names.length; i++) {
            try {
                gl = canvas.getContext(names[i], options);
            } catch (e) { }

            if (gl) {
                this.webgl2 = preferWebGl2 && i < 2;
                break;
            }
        }

        if (!gl) {
            throw new Error("WebGL not supported");
        }

        this.gl = gl;

        this.initializeExtensions();
        this.initializeCapabilities();
        this.initializeRenderState();

        for (i = 0; i < this.maxCombinedTextures; i++) {
            this.textureUnits.push([null, null, null]);
        }

        this.defaultClearOptions = {
            color: [0, 0, 0, 1],
            depth: 1,
            stencil: 0,
            flags: pc.CLEARFLAG_COLOR | pc.CLEARFLAG_DEPTH
        };

        this.glAddress = [
            gl.REPEAT,
            gl.CLAMP_TO_EDGE,
            gl.MIRRORED_REPEAT
        ];

        this.glBlendEquation = [
            gl.FUNC_ADD,
            gl.FUNC_SUBTRACT,
            gl.FUNC_REVERSE_SUBTRACT,
            this.webgl2 ? gl.MIN : this.extBlendMinmax ? this.extBlendMinmax.MIN_EXT : gl.FUNC_ADD,
            this.webgl2 ? gl.MAX : this.extBlendMinmax ? this.extBlendMinmax.MAX_EXT : gl.FUNC_ADD
        ];

        this.glBlendFunction = [
            gl.ZERO,
            gl.ONE,
            gl.SRC_COLOR,
            gl.ONE_MINUS_SRC_COLOR,
            gl.DST_COLOR,
            gl.ONE_MINUS_DST_COLOR,
            gl.SRC_ALPHA,
            gl.SRC_ALPHA_SATURATE,
            gl.ONE_MINUS_SRC_ALPHA,
            gl.DST_ALPHA,
            gl.ONE_MINUS_DST_ALPHA
        ];

        this.glComparison = [
            gl.NEVER,
            gl.LESS,
            gl.EQUAL,
            gl.LEQUAL,
            gl.GREATER,
            gl.NOTEQUAL,
            gl.GEQUAL,
            gl.ALWAYS
        ];

        this.glStencilOp = [
            gl.KEEP,
            gl.ZERO,
            gl.REPLACE,
            gl.INCR,
            gl.INCR_WRAP,
            gl.DECR,
            gl.DECR_WRAP,
            gl.INVERT
        ];

        this.glClearFlag = [
            0,
            gl.COLOR_BUFFER_BIT,
            gl.DEPTH_BUFFER_BIT,
            gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT,
            gl.STENCIL_BUFFER_BIT,
            gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT,
            gl.STENCIL_BUFFER_BIT | gl.DEPTH_BUFFER_BIT,
            gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT
        ];

        this.glCull = [
            0,
            gl.BACK,
            gl.FRONT,
            gl.FRONT_AND_BACK
        ];

        this.glFilter = [
            gl.NEAREST,
            gl.LINEAR,
            gl.NEAREST_MIPMAP_NEAREST,
            gl.NEAREST_MIPMAP_LINEAR,
            gl.LINEAR_MIPMAP_NEAREST,
            gl.LINEAR_MIPMAP_LINEAR
        ];

        this.glPrimitive = [
            gl.POINTS,
            gl.LINES,
            gl.LINE_LOOP,
            gl.LINE_STRIP,
            gl.TRIANGLES,
            gl.TRIANGLE_STRIP,
            gl.TRIANGLE_FAN
        ];

        this.glType = [
            gl.BYTE,
            gl.UNSIGNED_BYTE,
            gl.SHORT,
            gl.UNSIGNED_SHORT,
            gl.INT,
            gl.UNSIGNED_INT,
            gl.FLOAT
        ];

        this.pcUniformType = {};
        this.pcUniformType[gl.BOOL]         = pc.UNIFORMTYPE_BOOL;
        this.pcUniformType[gl.INT]          = pc.UNIFORMTYPE_INT;
        this.pcUniformType[gl.FLOAT]        = pc.UNIFORMTYPE_FLOAT;
        this.pcUniformType[gl.FLOAT_VEC2]   = pc.UNIFORMTYPE_VEC2;
        this.pcUniformType[gl.FLOAT_VEC3]   = pc.UNIFORMTYPE_VEC3;
        this.pcUniformType[gl.FLOAT_VEC4]   = pc.UNIFORMTYPE_VEC4;
        this.pcUniformType[gl.INT_VEC2]     = pc.UNIFORMTYPE_IVEC2;
        this.pcUniformType[gl.INT_VEC3]     = pc.UNIFORMTYPE_IVEC3;
        this.pcUniformType[gl.INT_VEC4]     = pc.UNIFORMTYPE_IVEC4;
        this.pcUniformType[gl.BOOL_VEC2]    = pc.UNIFORMTYPE_BVEC2;
        this.pcUniformType[gl.BOOL_VEC3]    = pc.UNIFORMTYPE_BVEC3;
        this.pcUniformType[gl.BOOL_VEC4]    = pc.UNIFORMTYPE_BVEC4;
        this.pcUniformType[gl.FLOAT_MAT2]   = pc.UNIFORMTYPE_MAT2;
        this.pcUniformType[gl.FLOAT_MAT3]   = pc.UNIFORMTYPE_MAT3;
        this.pcUniformType[gl.FLOAT_MAT4]   = pc.UNIFORMTYPE_MAT4;
        this.pcUniformType[gl.SAMPLER_2D]   = pc.UNIFORMTYPE_TEXTURE2D;
        this.pcUniformType[gl.SAMPLER_CUBE] = pc.UNIFORMTYPE_TEXTURECUBE;
        if (this.webgl2) {
            this.pcUniformType[gl.SAMPLER_2D_SHADOW]   = pc.UNIFORMTYPE_TEXTURE2D_SHADOW;
            this.pcUniformType[gl.SAMPLER_CUBE_SHADOW] = pc.UNIFORMTYPE_TEXTURECUBE_SHADOW;
            this.pcUniformType[gl.SAMPLER_3D]          = pc.UNIFORMTYPE_TEXTURE3D;
        }

        this.targetToSlot = {};
        this.targetToSlot[gl.TEXTURE_2D] = 0;
        this.targetToSlot[gl.TEXTURE_CUBE_MAP] = 1;
        this.targetToSlot[gl.TEXTURE_3D] = 2;

        // Define the uniform commit functions
        var scopeX, scopeY, scopeZ, scopeW;
        var uniformValue;
        this.commitFunction = [];
        this.commitFunction[pc.UNIFORMTYPE_BOOL] = function (uniform, value) {
            if (uniform.value !== value) {
                gl.uniform1i(uniform.locationId, value);
                uniform.value = value;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_INT] = this.commitFunction[pc.UNIFORMTYPE_BOOL];
        this.commitFunction[pc.UNIFORMTYPE_FLOAT] = function (uniform, value) {
            if (uniform.value !== value) {
                gl.uniform1f(uniform.locationId, value);
                uniform.value = value;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_VEC2]  = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) {
                gl.uniform2fv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_VEC3]  = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            scopeZ = value[2];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) {
                gl.uniform3fv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
                uniformValue[2] = scopeZ;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_VEC4]  = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            scopeZ = value[2];
            scopeW = value[3];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) {
                gl.uniform4fv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
                uniformValue[2] = scopeZ;
                uniformValue[3] = scopeW;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_IVEC2] = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) {
                gl.uniform2iv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_BVEC2] = this.commitFunction[pc.UNIFORMTYPE_IVEC2];
        this.commitFunction[pc.UNIFORMTYPE_IVEC3] = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            scopeZ = value[2];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) {
                gl.uniform3iv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
                uniformValue[2] = scopeZ;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_BVEC3] = this.commitFunction[pc.UNIFORMTYPE_IVEC3];
        this.commitFunction[pc.UNIFORMTYPE_IVEC4] = function (uniform, value) {
            uniformValue = uniform.value;
            scopeX = value[0];
            scopeY = value[1];
            scopeZ = value[2];
            scopeW = value[3];
            if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) {
                gl.uniform4iv(uniform.locationId, value);
                uniformValue[0] = scopeX;
                uniformValue[1] = scopeY;
                uniformValue[2] = scopeZ;
                uniformValue[3] = scopeW;
            }
        };
        this.commitFunction[pc.UNIFORMTYPE_BVEC4] = this.commitFunction[pc.UNIFORMTYPE_IVEC4];
        this.commitFunction[pc.UNIFORMTYPE_MAT2]  = function (uniform, value) {
            gl.uniformMatrix2fv(uniform.locationId, false, value);
        };
        this.commitFunction[pc.UNIFORMTYPE_MAT3]  = function (uniform, value) {
            gl.uniformMatrix3fv(uniform.locationId, false, value);
        };
        this.commitFunction[pc.UNIFORMTYPE_MAT4]  = function (uniform, value) {
            gl.uniformMatrix4fv(uniform.locationId, false, value);
        };
        this.commitFunction[pc.UNIFORMTYPE_FLOATARRAY] = function (uniform, value) {
            gl.uniform1fv(uniform.locationId, value);
        };

        // Create the ScopeNamespace for shader attributes and variables
        this.scope = new pc.ScopeSpace("Device");

        this.programLib = new pc.ProgramLibrary(this);
        for (var generator in pc.programlib)
            this.programLib.register(generator, pc.programlib[generator]);

        this.supportsBoneTextures = this.extTextureFloat && this.maxVertexTextures > 0;
        this.useTexCubeLod = this.extTextureLod && this.maxTextures < 16;

        // Calculate an estimate of the maximum number of bones that can be uploaded to the GPU
        // based on the number of available uniforms and the number of uniforms required for non-
        // bone data.  This is based off of the Standard shader.  A user defined shader may have
        // even less space available for bones so this calculated value can be overridden via
        // pc.GraphicsDevice.setBoneLimit.
        var numUniforms = this.vertexUniformsCount;
        numUniforms -= 4 * 4; // Model, view, projection and shadow matrices
        numUniforms -= 8;     // 8 lights max, each specifying a position vector
        numUniforms -= 1;     // Eye position
        numUniforms -= 4 * 4; // Up to 4 texture transforms
        this.boneLimit = Math.floor(numUniforms / 4);

        // Put a limit on the number of supported bones before skin partitioning must be performed
        // Some GPUs have demonstrated performance issues if the number of vectors allocated to the
        // skin matrix palette is left unbounded
        this.boneLimit = Math.min(this.boneLimit, 128);

        if (this.unmaskedRenderer === 'Mali-450 MP') {
            this.boneLimit = 34;
        }

        // Profiler stats
        this._drawCallsPerFrame = 0;
        this._shaderSwitchesPerFrame = 0;
        this._primsPerFrame = [];
        for (i = pc.PRIMITIVE_POINTS; i <= pc.PRIMITIVE_TRIFAN; i++) {
            this._primsPerFrame[i] = 0;
        }
        this._renderTargetCreationTime = 0;

        this._vram = {
            // #ifdef PROFILER
            texShadow: 0,
            texAsset: 0,
            texLightmap: 0,
            // #endif
            tex: 0,
            vb: 0,
            ib: 0
        };

        this._shaderStats = {
            vsCompiled: 0,
            fsCompiled: 0,
            linked: 0,
            materialShaders: 0,
            compileTime: 0
        };

        this.constantTexSource = this.scope.resolve("source");

        if (this.extTextureFloat) {
            if (this.webgl2) {
                // In WebGL2 float texture renderability is dictated by the EXT_color_buffer_float extension
                this.textureFloatRenderable = !!this.extColorBufferFloat;
            } else {
                // In WebGL1 we should just try rendering into a float texture
                this.textureFloatRenderable = testRenderable(gl, gl.FLOAT);
            }
        } else {
            this.textureFloatRenderable = false;
        }
        if (this.extTextureHalfFloat) {
            if (this.webgl2) {
                // EXT_color_buffer_float should affect both float and halffloat formats
                this.textureHalfFloatRenderable = !!this.extColorBufferFloat;
            } else {
                // Manual render check for half float
                this.textureHalfFloatRenderable = testRenderable(gl, this.extTextureHalfFloat.HALF_FLOAT_OES);
            }
        } else {
            this.textureHalfFloatRenderable = false;
        }

        this._textureFloatHighPrecision = undefined;

        this.createGrabPass(options.alpha);
    };
    GraphicsDevice.prototype = Object.create(pc.EventHandler.prototype);
    GraphicsDevice.prototype.constructor = GraphicsDevice;

    Object.assign(GraphicsDevice.prototype, {
        getPrecision: function () {
            var gl = this.gl;
            var precision = "highp";

            // Query the precision supported by ints and floats in vertex and fragment shaders.
            // Note that getShaderPrecisionFormat is not guaranteed to be present (such as some
            // instances of the default Android browser). In this case, assume highp is available.
            if (gl.getShaderPrecisionFormat) {
                var vertexShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT);
                var vertexShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT);

                var fragmentShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
                var fragmentShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT );

                var highpAvailable = vertexShaderPrecisionHighpFloat.precision > 0 && fragmentShaderPrecisionHighpFloat.precision > 0;
                var mediumpAvailable = vertexShaderPrecisionMediumpFloat.precision > 0 && fragmentShaderPrecisionMediumpFloat.precision > 0;

                if (!highpAvailable) {
                    if (mediumpAvailable) {
                        precision = "mediump";
                        // #ifdef DEBUG
                        console.warn("WARNING: highp not supported, using mediump");
                        // #endif
                    } else {
                        precision = "lowp";
                        // #ifdef DEBUG
                        console.warn( "WARNING: highp and mediump not supported, using lowp" );
                        // #endif
                    }
                }
            }

            return precision;
        },

        initializeExtensions: function () {
            var gl = this.gl;
            var ext;

            var supportedExtensions = gl.getSupportedExtensions();
            var getExtension = function () {
                var extension = null;
                for (var i = 0; i < arguments.length; i++) {
                    if (supportedExtensions.indexOf(arguments[i]) !== -1) {
                        extension = gl.getExtension(arguments[i]);
                    }
                }
                return extension;
            };

            if (this.webgl2) {
                this.extBlendMinmax = true;
                this.extDrawBuffers = true;
                this.extInstancing = true;
                this.extStandardDerivatives = true;
                this.extTextureFloat = true;
                this.extTextureHalfFloat = true;
                this.extTextureHalfFloatLinear = true;
                this.extTextureLod = true;
                this.extUintElement = true;
                this.extVertexArrayObject = true;
                this.extColorBufferFloat = getExtension('EXT_color_buffer_float');
            } else {
                this.extBlendMinmax = getExtension("EXT_blend_minmax");
                this.extDrawBuffers = getExtension('EXT_draw_buffers');
                this.extInstancing = getExtension("ANGLE_instanced_arrays");
                if (this.extInstancing) {
                    // Install the WebGL 2 Instancing API for WebGL 1.0
                    ext = this.extInstancing;
                    gl.drawArraysInstanced = ext.drawArraysInstancedANGLE.bind(ext);
                    gl.drawElementsInstanced = ext.drawElementsInstancedANGLE.bind(ext);
                    gl.vertexAttribDivisor = ext.vertexAttribDivisorANGLE.bind(ext);
                }

                this.extStandardDerivatives = getExtension("OES_standard_derivatives");
                this.extTextureFloat = getExtension("OES_texture_float");
                this.extTextureHalfFloat = getExtension("OES_texture_half_float");
                this.extTextureHalfFloatLinear = getExtension("OES_texture_half_float_linear");
                this.extTextureLod = getExtension('EXT_shader_texture_lod');
                this.extUintElement = getExtension("OES_element_index_uint");
                this.extVertexArrayObject = getExtension("OES_vertex_array_object");
                if (this.extVertexArrayObject) {
                    // Install the WebGL 2 VAO API for WebGL 1.0
                    ext = this.extVertexArrayObject;
                    gl.createVertexArray = ext.createVertexArrayOES.bind(ext);
                    gl.deleteVertexArray = ext.deleteVertexArrayOES.bind(ext);
                    gl.isVertexArray = ext.isVertexArrayOES.bind(ext);
                    gl.bindVertexArray = ext.bindVertexArrayOES.bind(ext);
                }
                this.extColorBufferFloat = null;
            }

            this.extDebugRendererInfo = getExtension('WEBGL_debug_renderer_info');
            this.extTextureFloatLinear = getExtension("OES_texture_float_linear");
            this.extTextureFilterAnisotropic = getExtension('EXT_texture_filter_anisotropic', 'WEBKIT_EXT_texture_filter_anisotropic');
            this.extCompressedTextureETC1 = getExtension('WEBGL_compressed_texture_etc1');
            this.extCompressedTextureETC = getExtension('WEBGL_compressed_texture_etc');
            this.extCompressedTexturePVRTC = getExtension('WEBGL_compressed_texture_pvrtc', 'WEBKIT_WEBGL_compressed_texture_pvrtc');
            this.extCompressedTextureS3TC = getExtension('WEBGL_compressed_texture_s3tc', 'WEBKIT_WEBGL_compressed_texture_s3tc');
            this.extCompressedTextureATC = getExtension('WEBGL_compressed_texture_atc');
            this.extCompressedTextureASTC = getExtension('WEBGL_compressed_texture_astc');
            this.extParallelShaderCompile = getExtension('KHR_parallel_shader_compile');
        },

        initializeCapabilities: function () {
            var gl = this.gl;
            var ext;

            this.maxPrecision = this.precision = this.getPrecision();

            var contextAttribs = gl.getContextAttributes();
            this.supportsMsaa = contextAttribs.antialias;
            this.supportsStencil = contextAttribs.stencil;

            // Query parameter values from the WebGL context
            this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
            this.maxCubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
            this.maxRenderBufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
            this.maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
            this.maxCombinedTextures = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
            this.maxVertexTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
            this.vertexUniformsCount = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
            this.fragmentUniformsCount = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
            if (this.webgl2) {
                this.maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS);
                this.maxColorAttachments = gl.getParameter(gl.MAX_COLOR_ATTACHMENTS);
                this.maxVolumeSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
            } else {
                ext = this.extDrawBuffers;
                this.maxDrawBuffers = ext ? gl.getParameter(ext.MAX_DRAW_BUFFERS_EXT) : 1;
                this.maxColorAttachments = ext ? gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_EXT) : 1;
                this.maxVolumeSize = 1;
            }

            ext = this.extDebugRendererInfo;
            this.unmaskedRenderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : '';
            this.unmaskedVendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : '';

            ext = this.extTextureFilterAnisotropic;
            this.maxAnisotropy = ext ? gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) : 1;

            this.samples = gl.getParameter(gl.SAMPLES);
        },

        initializeRenderState: function () {
            var gl = this.gl;

            // Initialize render state to a known start state
            this.blending = false;
            gl.disable(gl.BLEND);

            this.blendSrc = pc.BLENDMODE_ONE;
            this.blendDst = pc.BLENDMODE_ZERO;
            this.blendSrcAlpha = pc.BLENDMODE_ONE;
            this.blendDstAlpha = pc.BLENDMODE_ZERO;
            this.separateAlphaBlend = false;
            this.blendEquation = pc.BLENDEQUATION_ADD;
            this.blendAlphaEquation = pc.BLENDEQUATION_ADD;
            this.separateAlphaEquation = false;
            gl.blendFunc(gl.ONE, gl.ZERO);
            gl.blendEquation(gl.FUNC_ADD);

            this.writeRed = true;
            this.writeGreen = true;
            this.writeBlue = true;
            this.writeAlpha = true;
            gl.colorMask(true, true, true, true);

            this.cullMode = pc.CULLFACE_BACK;
            gl.enable(gl.CULL_FACE);
            gl.cullFace(gl.BACK);

            this.depthTest = true;
            gl.enable(gl.DEPTH_TEST);

            this.depthFunc = pc.FUNC_LESSEQUAL;
            gl.depthFunc(gl.LEQUAL);

            this.depthWrite = true;
            gl.depthMask(true);

            this.stencil = false;
            gl.disable(gl.STENCIL_TEST);

            this.stencilFuncFront = this.stencilFuncBack = pc.FUNC_ALWAYS;
            this.stencilRefFront = this.stencilRefBack = 0;
            this.stencilMaskFront = this.stencilMaskBack = 0xFF;
            gl.stencilFunc(gl.ALWAYS, 0, 0xFF);

            this.stencilFailFront = this.stencilFailBack = pc.STENCILOP_KEEP;
            this.stencilZfailFront = this.stencilZfailBack = pc.STENCILOP_KEEP;
            this.stencilZpassFront = this.stencilZpassBack = pc.STENCILOP_KEEP;
            this.stencilWriteMaskFront = 0xFF;
            this.stencilWriteMaskBack = 0xFF;
            gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
            gl.stencilMask(0xFF);

            this.alphaToCoverage = false;
            this.raster = true;
            if (this.webgl2) {
                gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
                gl.disable(gl.RASTERIZER_DISCARD);
            }

            this.depthBiasEnabled = false;
            gl.disable(gl.POLYGON_OFFSET_FILL);

            this.clearDepth = 1;
            gl.clearDepth(1);

            this.clearRed = 0;
            this.clearBlue = 0;
            this.clearGreen = 0;
            this.clearAlpha = 0;
            gl.clearColor(0, 0, 0, 0);

            this.clearStencil = 0;
            gl.clearStencil(0);

            // Cached viewport and scissor dimensions
            this.vx = this.vy = this.vw = this.vh = 0;
            this.sx = this.sy = this.sw = this.sh = 0;

            if (this.webgl2) {
                gl.hint(gl.FRAGMENT_SHADER_DERIVATIVE_HINT, gl.NICEST);
            } else {
                if (this.extStandardDerivatives) {
                    gl.hint(this.extStandardDerivatives.FRAGMENT_SHADER_DERIVATIVE_HINT_OES, gl.NICEST);
                }
            }

            gl.enable(gl.SCISSOR_TEST);

            gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);

            this.unpackFlipY = false;
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);

            this.unpackPremultiplyAlpha = false;
            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
        },

        initializeContext: function () {
            this.initializeExtensions();
            this.initializeCapabilities();
            this.initializeRenderState();

            // Recompile all shaders (they'll be linked when they're next actually used)
            var i, len;
            for (i = 0, len = this.shaders.length; i < len; i++) {
                this.compileAndLinkShader(this.shaders[i]);
            }
            this.shader = null;

            // Recreate buffer objects and reupload buffer data to the GPU
            for (i = 0, len = this.buffers.length; i < len; i++) {
                this.buffers[i].bufferId = undefined;
                this.buffers[i].unlock();
            }
            this.boundBuffer = null;
            this.boundElementBuffer = null;
            this.indexBuffer = null;
            this.attributesInvalidated = true;
            this.enabledAttributes = {};
            this.vertexBuffers = [];

            // Force all textures to be recreated and reuploaded
            for (i = 0, len = this.textures.length; i < len; i++) {
                var texture = this.textures[i];
                this.destroyTexture(texture);
                texture.dirtyAll();
            }
            this.textureUnit = 0;
            this.textureUnits.length = 0;
            for (i = 0; i < this.maxCombinedTextures; i++) {
                this.textureUnits.push([null, null, null]);
            }

            // Reset all render targets so they'll be recreated as required.
            // TODO: a solution for the case where a render target contains something
            // that was previously generated that needs to be re-rendered.
            for (i = 0, len = this.targets.length; i < len; i++) {
                this.targets[i]._glFrameBuffer = undefined;
                this.targets[i]._glDepthBuffer = undefined;
                this.targets[i]._glResolveFrameBuffer = undefined;
                this.targets[i]._glMsaaColorBuffer = undefined;
                this.targets[i]._glMsaaDepthBuffer = undefined;
            }
            this.renderTarget = null;
            this.activeFramebuffer = null;
            this.feedback = null;
            this.transformFeedbackBuffer = null;
        },

        createGrabPass: function (alpha) {
            if (this.grabPassTexture) return;

            var format = alpha ? pc.PIXELFORMAT_R8_G8_B8_A8 : pc.PIXELFORMAT_R8_G8_B8;

            var grabPassTexture = new pc.Texture(this, {
                format: format,
                minFilter: pc.FILTER_LINEAR,
                magFilter: pc.FILTER_LINEAR,
                addressU: pc.ADDRESS_CLAMP_TO_EDGE,
                addressV: pc.ADDRESS_CLAMP_TO_EDGE,
                mipmaps: false
            });

            grabPassTexture.name = 'texture_grabPass';

            var grabPassTextureId = this.scope.resolve(grabPassTexture.name);
            grabPassTextureId.setValue(grabPassTexture);

            this.grabPassTextureId = grabPassTextureId;
            this.grabPassTexture = grabPassTexture;
        },

        updateGrabPass: function () {
            var gl = this.gl;

            var format = this.grabPassTexture._glFormat;
            var source = (this.renderTarget && this.renderTarget.colorBuffer) || this.canvas;
            gl.copyTexImage2D(gl.TEXTURE_2D, 0, format, 0, 0, source.width, source.height, 0);
            this.grabPassTexture._width = source.width;
            this.grabPassTexture._height = source.height;
        },

        destroyGrabPass: function () {
            this.grabPassTexture.destroy();
            this.grabPassTexture = null;
            this.grabPassTextureId = null;
        },

        updateClientRect: function () {
            this.clientRect = this.canvas.getBoundingClientRect();
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setViewport
         * @description Set the active rectangle for rendering on the specified device.
         * @param {Number} x The pixel space x-coordinate of the bottom left corner of the viewport.
         * @param {Number} y The pixel space y-coordinate of the bottom left corner of the viewport.
         * @param {Number} w The width of the viewport in pixels.
         * @param {Number} h The height of the viewport in pixels.
         */
        setViewport: function (x, y, w, h) {
            if ((this.vx !== x) || (this.vy !== y) || (this.vw !== w) || (this.vh !== h)) {
                this.gl.viewport(x, y, w, h);
                this.vx = x;
                this.vy = y;
                this.vw = w;
                this.vh = h;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setScissor
         * @description Set the active scissor rectangle on the specified device.
         * @param {Number} x The pixel space x-coordinate of the bottom left corner of the scissor rectangle.
         * @param {Number} y The pixel space y-coordinate of the bottom left corner of the scissor rectangle.
         * @param {Number} w The width of the scissor rectangle in pixels.
         * @param {Number} h The height of the scissor rectangle in pixels.
         */
        setScissor: function (x, y, w, h) {
            if ((this.sx !== x) || (this.sy !== y) || (this.sw !== w) || (this.sh !== h)) {
                this.gl.scissor(x, y, w, h);
                this.sx = x;
                this.sy = y;
                this.sw = w;
                this.sh = h;
            }
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#getProgramLibrary
         * @description Retrieves the program library assigned to the specified graphics device.
         * @returns {pc.ProgramLibrary} The program library assigned to the device.
         */
        getProgramLibrary: function () {
            return this.programLib;
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#setProgramLibrary
         * @description Assigns a program library to the specified device. By default, a graphics
         * device is created with a program library that manages all of the programs that are
         * used to render any graphical primitives. However, this function allows the user to
         * replace the existing program library with a new one.
         * @param {pc.ProgramLibrary} programLib The program library to assign to the device.
         */
        setProgramLibrary: function (programLib) {
            this.programLib = programLib;
        },

        setFramebuffer: function (fb) {
            if (this.activeFramebuffer !== fb) {
                this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fb);
                this.activeFramebuffer = fb;
            }
        },

        _checkFbo: function () {
            // Ensure all is well
            var gl = this.gl;
            var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
            switch (status) {
                case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                    console.error("ERROR: FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
                    break;
                case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
                    console.error("ERROR: FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
                    break;
                case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
                    console.error("ERROR: FRAMEBUFFER_INCOMPLETE_DIMENSIONS");
                    break;
                case gl.FRAMEBUFFER_UNSUPPORTED:
                    console.error("ERROR: FRAMEBUFFER_UNSUPPORTED");
                    break;
                case gl.FRAMEBUFFER_COMPLETE:
                    break;
                default:
                    break;
            }
        },

        copyRenderTarget: function (source, dest, color, depth) {
            var gl = this.gl;

            if (!this.webgl2 && depth) {
                // #ifdef DEBUG
                console.error("Depth is not copyable on WebGL 1.0");
                // #endif
                return false;
            }
            if (color) {
                if (!dest) {
                    // copying to backbuffer
                    if (!source._colorBuffer) {
                        // #ifdef DEBUG
                        console.error("Can't copy empty color buffer to backbuffer");
                        // #endif
                        return false;
                    }
                } else {
                    // copying to render target
                    if (!source._colorBuffer || !dest._colorBuffer) {
                        // #ifdef DEBUG
                        console.error("Can't copy color buffer, because one of the render targets doesn't have it");
                        // #endif
                        return false;
                    }
                    if (source._colorBuffer._format !== dest._colorBuffer._format) {
                        // #ifdef DEBUG
                        console.error("Can't copy render targets of different color formats");
                        // #endif
                        return false;
                    }
                }
            }
            if (depth) {
                if (!source._depthBuffer || !dest._depthBuffer) {
                    // #ifdef DEBUG
                    console.error("Can't copy depth buffer, because one of the render targets doesn't have it");
                    // #endif
                    return false;
                }
                if (source._depthBuffer._format !== dest._depthBuffer._format) {
                    // #ifdef DEBUG
                    console.error("Can't copy render targets of different depth formats");
                    // #endif
                    return false;
                }
            }

            if (this.webgl2 && dest) {
                var prevRt = this.renderTarget;
                this.renderTarget = dest;
                this.updateBegin();
                gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source ? source._glFrameBuffer : null);
                gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dest._glFrameBuffer);
                var w = source ? source.width : dest.width;
                var h = source ? source.height : dest.height;
                gl.blitFramebuffer( 0, 0, w, h,
                                    0, 0, w, h,
                                    (color ? gl.COLOR_BUFFER_BIT : 0) | (depth ? gl.DEPTH_BUFFER_BIT : 0),
                                    gl.NEAREST);
                this.renderTarget = prevRt;
                gl.bindFramebuffer(gl.FRAMEBUFFER, prevRt ? prevRt._glFrameBuffer : null);
            } else {
                if (!this._copyShader) {
                    var chunks = pc.shaderChunks;
                    this._copyShader = chunks.createShaderFromCode(this, chunks.fullscreenQuadVS, chunks.outputTex2DPS, "outputTex2D");
                }
                this.constantTexSource.setValue(source._colorBuffer);
                pc.drawQuadWithShader(this, dest, this._copyShader);
            }

            return true;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#updateBegin
         * @description Marks the beginning of a block of rendering. Internally, this function
         * binds the render target currently set on the device. This function should be matched
         * with a call to pc.GraphicsDevice#updateEnd. Calls to pc.GraphicsDevice#updateBegin
         * and pc.GraphicsDevice#updateEnd must not be nested.
         */
        updateBegin: function () {
            var gl = this.gl;

            this.boundBuffer = null;
            this.boundElementBuffer = null;

            // Set the render target
            var target = this.renderTarget;
            if (target) {
                // Create a new WebGL frame buffer object
                if (!target._glFrameBuffer) {

                    // #ifdef PROFILER
                    var startTime = pc.now();
                    this.fire('fbo:create', {
                        timestamp: startTime,
                        target: this
                    });
                    // #endif

                    // Set RT's device
                    target._device = this;

                    // ##### Create main FBO #####
                    target._glFrameBuffer = gl.createFramebuffer();
                    this.setFramebuffer(target._glFrameBuffer);

                    // --- Init the provided color buffer (optional) ---
                    var colorBuffer = target._colorBuffer;
                    if (colorBuffer) {
                        if (!colorBuffer._glTexture) {
                            // Clamp the render buffer size to the maximum supported by the device
                            colorBuffer._width = Math.min(colorBuffer.width, this.maxRenderBufferSize);
                            colorBuffer._height = Math.min(colorBuffer.height, this.maxRenderBufferSize);
                            this.setTexture(colorBuffer, 0);
                        }
                        // Attach the color buffer
                        gl.framebufferTexture2D(
                            gl.FRAMEBUFFER,
                            gl.COLOR_ATTACHMENT0,
                            colorBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D,
                            colorBuffer._glTexture,
                            0
                        );
                    }

                    var depthBuffer = target._depthBuffer;
                    if (depthBuffer && this.webgl2) {
                        // --- Init the provided depth/stencil buffer (optional, WebGL2 only) ---
                        if (!depthBuffer._glTexture) {
                            // Clamp the render buffer size to the maximum supported by the device
                            depthBuffer._width = Math.min(depthBuffer.width, this.maxRenderBufferSize);
                            depthBuffer._height = Math.min(depthBuffer.height, this.maxRenderBufferSize);
                            this.setTexture(depthBuffer, 0);
                        }
                        // Attach
                        if (target._stencil) {
                            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT,
                                                    depthBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D,
                                                    target._depthBuffer._glTexture, 0);
                        } else {
                            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
                                                    depthBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D,
                                                    target._depthBuffer._glTexture, 0);
                        }
                    } else if (target._depth) {
                        // --- Init a new depth/stencil buffer (optional) ---
                        // if this is a MSAA RT, and no buffer to resolve to, skip creating non-MSAA depth
                        var willRenderMsaa = target._samples > 1 && this.webgl2;
                        if (!willRenderMsaa) {
                            if (!target._glDepthBuffer) {
                                target._glDepthBuffer = gl.createRenderbuffer();
                            }
                            gl.bindRenderbuffer(gl.RENDERBUFFER, target._glDepthBuffer);
                            if (target._stencil) {
                                gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, target.width, target.height);
                                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, target._glDepthBuffer);
                            } else {
                                gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, target.width, target.height);
                                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, target._glDepthBuffer);
                            }
                            gl.bindRenderbuffer(gl.RENDERBUFFER, null);
                        }
                    }

                    // #ifdef DEBUG
                    this._checkFbo();
                    // #endif

                    // ##### Create MSAA FBO (WebGL2 only) #####
                    if (this.webgl2 && target._samples > 1) {

                        // Use previous FBO for resolves
                        target._glResolveFrameBuffer = target._glFrameBuffer;

                        // Actual FBO will be MSAA
                        target._glFrameBuffer = gl.createFramebuffer();
                        this.setFramebuffer(target._glFrameBuffer);

                        // Create an optional MSAA color buffer
                        if (colorBuffer) {
                            if (!target._glMsaaColorBuffer) {
                                target._glMsaaColorBuffer = gl.createRenderbuffer();
                            }
                            gl.bindRenderbuffer(gl.RENDERBUFFER, target._glMsaaColorBuffer);
                            gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, colorBuffer._glInternalFormat, target.width, target.height);
                            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, target._glMsaaColorBuffer);
                        }

                        // Optionally add a MSAA depth/stencil buffer
                        if (target._depth) {
                            if (!target._glMsaaDepthBuffer) {
                                target._glMsaaDepthBuffer = gl.createRenderbuffer();
                            }
                            gl.bindRenderbuffer(gl.RENDERBUFFER, target._glMsaaDepthBuffer);
                            if (target._stencil) {
                                gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, gl.DEPTH24_STENCIL8, target.width, target.height);
                                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, target._glMsaaDepthBuffer);
                            } else {
                                gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, gl.DEPTH_COMPONENT32F, target.width, target.height);
                                gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, target._glMsaaDepthBuffer);
                            }
                        }
                        // #ifdef DEBUG
                        this._checkFbo();
                        // #endif
                    }

                    this.targets.push(target);

                    // #ifdef PROFILER
                    this._renderTargetCreationTime += pc.now() - startTime;
                    // #endif

                } else {
                    this.setFramebuffer(target._glFrameBuffer);
                }
            } else {
                this.setFramebuffer(null);
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#updateEnd
         * @description Marks the end of a block of rendering. This function should be called
         * after a matching call to pc.GraphicsDevice#updateBegin. Calls to pc.GraphicsDevice#updateBegin
         * and pc.GraphicsDevice#updateEnd must not be nested.
         */
        updateEnd: function () {
            var gl = this.gl;

            // Unset the render target
            var target = this.renderTarget;
            if (target) {
                // If the active render target is auto-mipmapped, generate its mip chain
                var colorBuffer = target._colorBuffer;
                if (colorBuffer && colorBuffer._glTexture && colorBuffer.mipmaps && colorBuffer.pot) {
                    this.activeTexture(this.maxCombinedTextures - 1);
                    this.bindTexture(colorBuffer);
                    gl.generateMipmap(colorBuffer._glTarget);
                }

                // Resolve MSAA if needed
                if (this.webgl2 && target._samples > 1 && target.autoResolve) {
                    target.resolve();
                }
            }
        },

        initializeTexture: function (texture) {
            var gl = this.gl;
            var ext;

            texture._glTexture = gl.createTexture();

            texture._glTarget = texture._cubemap ? gl.TEXTURE_CUBE_MAP :
                (texture._volume ? gl.TEXTURE_3D : gl.TEXTURE_2D);

            switch (texture._format) {
                case pc.PIXELFORMAT_A8:
                    texture._glFormat = gl.ALPHA;
                    texture._glInternalFormat = gl.ALPHA;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_L8:
                    texture._glFormat = gl.LUMINANCE;
                    texture._glInternalFormat = gl.LUMINANCE;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_L8_A8:
                    texture._glFormat = gl.LUMINANCE_ALPHA;
                    texture._glInternalFormat = gl.LUMINANCE_ALPHA;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_R5_G6_B5:
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = gl.RGB;
                    texture._glPixelType = gl.UNSIGNED_SHORT_5_6_5;
                    break;
                case pc.PIXELFORMAT_R5_G5_B5_A1:
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = gl.RGBA;
                    texture._glPixelType = gl.UNSIGNED_SHORT_5_5_5_1;
                    break;
                case pc.PIXELFORMAT_R4_G4_B4_A4:
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = gl.RGBA;
                    texture._glPixelType = gl.UNSIGNED_SHORT_4_4_4_4;
                    break;
                case pc.PIXELFORMAT_R8_G8_B8:
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = this.webgl2 ? gl.RGB8 : gl.RGB;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_R8_G8_B8_A8:
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = this.webgl2 ? gl.RGBA8 : gl.RGBA;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_DXT1:
                    ext = this.extCompressedTextureS3TC;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB_S3TC_DXT1_EXT;
                    break;
                case pc.PIXELFORMAT_DXT3:
                    ext = this.extCompressedTextureS3TC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
                    break;
                case pc.PIXELFORMAT_DXT5:
                    ext = this.extCompressedTextureS3TC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
                    break;
                case pc.PIXELFORMAT_ETC1:
                    ext = this.extCompressedTextureETC1;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB_ETC1_WEBGL;
                    break;
                case pc.PIXELFORMAT_PVRTC_2BPP_RGB_1:
                    ext = this.extCompressedTexturePVRTC;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
                    break;
                case pc.PIXELFORMAT_PVRTC_2BPP_RGBA_1:
                    ext = this.extCompressedTexturePVRTC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
                    break;
                case pc.PIXELFORMAT_PVRTC_4BPP_RGB_1:
                    ext = this.extCompressedTexturePVRTC;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
                    break;
                case pc.PIXELFORMAT_PVRTC_4BPP_RGBA_1:
                    ext = this.extCompressedTexturePVRTC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
                    break;
                case pc.PIXELFORMAT_ETC2_RGB:
                    ext = this.extCompressedTextureETC;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB8_ETC2;
                    break;
                case pc.PIXELFORMAT_ETC2_RGBA:
                    ext = this.extCompressedTextureETC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA8_ETC2_EAC;
                    break;
                case pc.PIXELFORMAT_ASTC_4x4:
                    ext = this.extCompressedTextureASTC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_ASTC_4x4_KHR;
                    break;
                case pc.PIXELFORMAT_ATC_RGB:
                    ext = this.extCompressedTextureATC;
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = ext.COMPRESSED_RGB_ATC_WEBGL;
                    break;
                case pc.PIXELFORMAT_ATC_RGBA:
                    ext = this.extCompressedTextureATC;
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = ext.COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL;
                    break;
                case pc.PIXELFORMAT_RGB16F:
                    // definition varies between WebGL1 and 2
                    ext = this.extTextureHalfFloat;
                    texture._glFormat = gl.RGB;
                    if (this.webgl2) {
                        texture._glInternalFormat = gl.RGB16F;
                        texture._glPixelType = gl.HALF_FLOAT;
                    } else {
                        texture._glInternalFormat = gl.RGB;
                        texture._glPixelType = ext.HALF_FLOAT_OES;
                    }
                    break;
                case pc.PIXELFORMAT_RGBA16F:
                    // definition varies between WebGL1 and 2
                    ext = this.extTextureHalfFloat;
                    texture._glFormat = gl.RGBA;
                    if (this.webgl2) {
                        texture._glInternalFormat = gl.RGBA16F;
                        texture._glPixelType = gl.HALF_FLOAT;
                    } else {
                        texture._glInternalFormat = gl.RGBA;
                        texture._glPixelType = ext.HALF_FLOAT_OES;
                    }
                    break;
                case pc.PIXELFORMAT_RGB32F:
                    // definition varies between WebGL1 and 2
                    texture._glFormat = gl.RGB;
                    if (this.webgl2) {
                        texture._glInternalFormat = gl.RGB32F;
                    } else {
                        texture._glInternalFormat = gl.RGB;
                    }
                    texture._glPixelType = gl.FLOAT;
                    break;
                case pc.PIXELFORMAT_RGBA32F:
                    // definition varies between WebGL1 and 2
                    texture._glFormat = gl.RGBA;
                    if (this.webgl2) {
                        texture._glInternalFormat = gl.RGBA32F;
                    } else {
                        texture._glInternalFormat = gl.RGBA;
                    }
                    texture._glPixelType = gl.FLOAT;
                    break;
                case pc.PIXELFORMAT_R32F: // WebGL2 only
                    texture._glFormat = gl.RED;
                    texture._glInternalFormat = gl.R32F;
                    texture._glPixelType = gl.FLOAT;
                    break;
                case pc.PIXELFORMAT_DEPTH:
                    if (this.webgl2) {
                        // native WebGL2
                        texture._glFormat = gl.DEPTH_COMPONENT;
                        texture._glInternalFormat = gl.DEPTH_COMPONENT32F; // should allow 16/24 bits?
                        texture._glPixelType = gl.FLOAT;
                    } else {
                        // using WebGL1 extension
                        texture._glFormat = gl.DEPTH_COMPONENT;
                        texture._glInternalFormat = gl.DEPTH_COMPONENT;
                        texture._glPixelType = gl.UNSIGNED_SHORT; // the only acceptable value?
                    }
                    break;
                case pc.PIXELFORMAT_DEPTHSTENCIL: // WebGL2 only
                    texture._glFormat = gl.DEPTH_STENCIL;
                    texture._glInternalFormat = gl.DEPTH24_STENCIL8;
                    texture._glPixelType = gl.UNSIGNED_INT_24_8;
                    break;
                case pc.PIXELFORMAT_111110F: // WebGL2 only
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = gl.R11F_G11F_B10F;
                    texture._glPixelType = gl.FLOAT;
                    break;
                case pc.PIXELFORMAT_SRGB: // WebGL2 only
                    texture._glFormat = gl.RGB;
                    texture._glInternalFormat = gl.SRGB8;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
                case pc.PIXELFORMAT_SRGBA: // WebGL2 only
                    texture._glFormat = gl.RGBA;
                    texture._glInternalFormat = gl.SRGB8_ALPHA8;
                    texture._glPixelType = gl.UNSIGNED_BYTE;
                    break;
            }

            // Track this texture now that it is a WebGL resource
            this.textures.push(texture);
        },

        destroyTexture: function (texture) {
            if (texture._glTexture) {
                // Remove texture from device's texture cache
                var idx = this.textures.indexOf(texture);
                if (idx !== -1) {
                    this.textures.splice(idx, 1);
                }

                // Remove texture from any uniforms
                for (var uniformName in this.scope.variables) {
                    var uniform = this.scope.variables[uniformName];
                    if (uniform.value === texture) {
                        uniform.value = null;
                    }
                }

                // Update shadowed texture unit state to remove texture from any units
                for (var i = 0; i < this.textureUnits.length; i++) {
                    var textureUnit = this.textureUnits[i];
                    for (var j = 0; j < textureUnit.length; j++) {
                        if (textureUnit[j] === texture._glTexture) {
                            textureUnit[j] = null;
                        }
                    }
                }

                // Blow away WebGL texture resource
                var gl = this.gl;
                gl.deleteTexture(texture._glTexture);
                delete texture._glTexture;
                delete texture._glTarget;
                delete texture._glFormat;
                delete texture._glInternalFormat;
                delete texture._glPixelType;

                // Update texture stats
                this._vram.tex -= texture._gpuSize;
                // #ifdef PROFILER
                if (texture.profilerHint === pc.TEXHINT_SHADOWMAP) {
                    this._vram.texShadow -= texture._gpuSize;
                } else if (texture.profilerHint === pc.TEXHINT_ASSET) {
                    this._vram.texAsset -= texture._gpuSize;
                } else if (texture.profilerHint === pc.TEXHINT_LIGHTMAP) {
                    this._vram.texLightmap -= texture._gpuSize;
                }
                // #endif
            }
        },

        setUnpackFlipY: function (flipY) {
            if (this.unpackFlipY !== flipY) {
                this.unpackFlipY = flipY;

                // Note: the WebGL spec states that UNPACK_FLIP_Y_WEBGL only affects
                // texImage2D and texSubImage2D, not compressedTexImage2D
                var gl = this.gl;
                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);
            }
        },

        setUnpackPremultiplyAlpha: function (premultiplyAlpha) {
            if (this.unpackPremultiplyAlpha !== premultiplyAlpha) {
                this.unpackPremultiplyAlpha = premultiplyAlpha;

                // Note: the WebGL spec states that UNPACK_PREMULTIPLY_ALPHA_WEBGL only affects
                // texImage2D and texSubImage2D, not compressedTexImage2D
                var gl = this.gl;
                gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiplyAlpha);
            }
        },

        uploadTexture: function (texture) {
            var gl = this.gl;

            if (!texture._needsUpload && ((texture._needsMipmapsUpload && texture._mipmapsUploaded) || !texture.pot))
                return;

            var mipLevel = 0;
            var mipObject;
            var resMult;

            var requiredMipLevels = Math.log2(Math.max(texture._width, texture._height)) + 1;

            while (texture._levels[mipLevel] || mipLevel === 0) {
                // Upload all existing mip levels. Initialize 0 mip anyway.

                if (!texture._needsUpload && mipLevel === 0) {
                    mipLevel++;
                    continue;
                } else if (mipLevel && (!texture._needsMipmapsUpload || !texture._mipmaps)) {
                    break;
                }

                mipObject = texture._levels[mipLevel];

                if (mipLevel == 1 && !texture._compressed && texture._levels.length < requiredMipLevels) {
                    // We have more than one mip levels we want to assign, but we need all mips to make
                    // the texture complete. Therefore first generate all mip chain from 0, then assign custom mips.
                    // (this implies the call to _completePartialMipLevels above was unsuccessful)
                    gl.generateMipmap(texture._glTarget);
                    texture._mipmapsUploaded = true;
                }

                if (texture._cubemap) {
                    // ----- CUBEMAP -----
                    var face;

                    if ((mipObject[0] instanceof HTMLCanvasElement) || (mipObject[0] instanceof HTMLImageElement) || (mipObject[0] instanceof HTMLVideoElement)) {
                        // Upload the image, canvas or video
                        for (face = 0; face < 6; face++) {
                            if (!texture._levelsUpdated[0][face])
                                continue;

                            var src = mipObject[face];
                            // Downsize images that are too large to be used as cube maps
                            if (src instanceof HTMLImageElement) {
                                if (src.width > this.maxCubeMapSize || src.height > this.maxCubeMapSize) {
                                    src = _downsampleImage(src, this.maxCubeMapSize);
                                    if (mipLevel === 0) {
                                        texture.width = src.width;
                                        texture.height = src.height;
                                    }
                                }
                            }

                            this.setUnpackFlipY(false);
                            this.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
                            gl.texImage2D(
                                gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
                                mipLevel,
                                texture._glInternalFormat,
                                texture._glFormat,
                                texture._glPixelType,
                                src
                            );
                        }
                    } else {
                        // Upload the byte array
                        resMult = 1 / Math.pow(2, mipLevel);
                        for (face = 0; face < 6; face++) {
                            if (!texture._levelsUpdated[0][face])
                                continue;

                            var texData = mipObject[face];
                            if (texture._compressed) {
                                gl.compressedTexImage2D(
                                    gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
                                    mipLevel,
                                    texture._glInternalFormat,
                                    Math.max(texture._width * resMult, 1),
                                    Math.max(texture._height * resMult, 1),
                                    0,
                                    texData
                                );
                            } else {
                                this.setUnpackFlipY(false);
                                this.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
                                gl.texImage2D(
                                    gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
                                    mipLevel,
                                    texture._glInternalFormat,
                                    Math.max(texture._width * resMult, 1),
                                    Math.max(texture._height * resMult, 1),
                                    0,
                                    texture._glFormat,
                                    texture._glPixelType,
                                    texData
                                );
                            }
                        }
                    }
                } else if (texture._volume) {
                    // ----- 3D -----
                    // Image/canvas/video not supported (yet?)
                    // Upload the byte array
                    resMult = 1 / Math.pow(2, mipLevel);
                    if (texture._compressed) {
                        gl.compressedTexImage3D(gl.TEXTURE_3D,
                                                mipLevel,
                                                texture._glInternalFormat,
                                                Math.max(texture._width * resMult, 1),
                                                Math.max(texture._height * resMult, 1),
                                                Math.max(texture._depth * resMult, 1),
                                                0,
                                                mipObject);
                    } else {
                        this.setUnpackFlipY(false);
                        this.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
                        gl.texImage3D(gl.TEXTURE_3D,
                                      mipLevel,
                                      texture._glInternalFormat,
                                      Math.max(texture._width * resMult, 1),
                                      Math.max(texture._height * resMult, 1),
                                      Math.max(texture._depth * resMult, 1),
                                      0,
                                      texture._glFormat,
                                      texture._glPixelType,
                                      mipObject);
                    }
                } else {
                    // ----- 2D -----
                    if ((mipObject instanceof HTMLCanvasElement) || (mipObject instanceof HTMLImageElement) || (mipObject instanceof HTMLVideoElement)) {
                        // Downsize images that are too large to be used as textures
                        if (mipObject instanceof HTMLImageElement) {
                            if (mipObject.width > this.maxTextureSize || mipObject.height > this.maxTextureSize) {
                                mipObject = _downsampleImage(mipObject, this.maxTextureSize);
                                if (mipLevel === 0) {
                                    texture.width = mipObject.width;
                                    texture.height = mipObject.height;
                                }
                            }
                        }

                        // Upload the image, canvas or video
                        this.setUnpackFlipY(texture._flipY);
                        this.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
                        gl.texImage2D(
                            gl.TEXTURE_2D,
                            mipLevel,
                            texture._glInternalFormat,
                            texture._glFormat,
                            texture._glPixelType,
                            mipObject
                        );
                    } else {
                        // Upload the byte array
                        resMult = 1 / Math.pow(2, mipLevel);
                        if (texture._compressed) {
                            gl.compressedTexImage2D(
                                gl.TEXTURE_2D,
                                mipLevel,
                                texture._glInternalFormat,
                                Math.max(texture._width * resMult, 1),
                                Math.max(texture._height * resMult, 1),
                                0,
                                mipObject
                            );
                        } else {
                            this.setUnpackFlipY(false);
                            this.setUnpackPremultiplyAlpha(texture._premultiplyAlpha);
                            gl.texImage2D(
                                gl.TEXTURE_2D,
                                mipLevel,
                                texture._glInternalFormat,
                                Math.max(texture._width * resMult, 1),
                                Math.max(texture._height * resMult, 1),
                                0,
                                texture._glFormat,
                                texture._glPixelType,
                                mipObject
                            );
                        }
                    }

                    if (mipLevel === 0) {
                        texture._mipmapsUploaded = false;
                    } else {
                        texture._mipmapsUploaded = true;
                    }
                }
                mipLevel++;
            }

            if (texture._needsUpload) {
                if (texture._cubemap) {
                    for (var i = 0; i < 6; i++)
                        texture._levelsUpdated[0][i] = false;
                } else {
                    texture._levelsUpdated[0] = false;
                }
            }

            if (!texture._compressed && texture._mipmaps && texture._needsMipmapsUpload && texture.pot && texture._levels.length === 1) {
                gl.generateMipmap(texture._glTarget);
                texture._mipmapsUploaded = true;
            }

            if (texture._gpuSize) {
                this._vram.tex -= texture._gpuSize;
                // #ifdef PROFILER
                if (texture.profilerHint === pc.TEXHINT_SHADOWMAP) {
                    this._vram.texShadow -= texture._gpuSize;
                } else if (texture.profilerHint === pc.TEXHINT_ASSET) {
                    this._vram.texAsset -= texture._gpuSize;
                } else if (texture.profilerHint === pc.TEXHINT_LIGHTMAP) {
                    this._vram.texLightmap -= texture._gpuSize;
                }
                // #endif
            }

            texture._gpuSize = texture.gpuSize;
            this._vram.tex += texture._gpuSize;
            // #ifdef PROFILER
            if (texture.profilerHint === pc.TEXHINT_SHADOWMAP) {
                this._vram.texShadow += texture._gpuSize;
            } else if (texture.profilerHint === pc.TEXHINT_ASSET) {
                this._vram.texAsset += texture._gpuSize;
            } else if (texture.profilerHint === pc.TEXHINT_LIGHTMAP) {
                this._vram.texLightmap += texture._gpuSize;
            }
            // #endif
        },

        // Activate the specified texture unit
        activeTexture: function (textureUnit) {
            if (this.textureUnit !== textureUnit) {
                this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit);
                this.textureUnit = textureUnit;
            }
        },

        // If the texture is not already bound on the currently active texture
        // unit, bind it
        bindTexture: function (texture) {
            var textureTarget = texture._glTarget;
            var textureObject = texture._glTexture;
            var textureUnit = this.textureUnit;
            var slot = this.targetToSlot[textureTarget];
            if (this.textureUnits[textureUnit][slot] !== textureObject) {
                this.gl.bindTexture(textureTarget, textureObject);
                this.textureUnits[textureUnit][slot] = textureObject;
            }
        },

        // If the texture is not bound on the specified texture unit, active the
        // texture unit and bind the texture to it
        bindTextureOnUnit: function (texture, textureUnit) {
            var textureTarget = texture._glTarget;
            var textureObject = texture._glTexture;
            var slot = this.targetToSlot[textureTarget];
            if (this.textureUnits[textureUnit][slot] !== textureObject) {
                this.activeTexture(textureUnit);
                this.gl.bindTexture(textureTarget, textureObject);
                this.textureUnits[textureUnit][slot] = textureObject;
            }
        },

        setTextureParameters: function (texture) {
            var gl = this.gl;
            var flags = texture._parameterFlags;
            var target = texture._glTarget;

            if (flags & 1) {
                var filter = texture._minFilter;
                if (!texture.pot || !texture._mipmaps || (texture._compressed && texture._levels.length === 1)) {
                    if (filter === pc.FILTER_NEAREST_MIPMAP_NEAREST || filter === pc.FILTER_NEAREST_MIPMAP_LINEAR) {
                        filter = pc.FILTER_NEAREST;
                    } else if (filter === pc.FILTER_LINEAR_MIPMAP_NEAREST || filter === pc.FILTER_LINEAR_MIPMAP_LINEAR) {
                        filter = pc.FILTER_LINEAR;
                    }
                }
                gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, this.glFilter[filter]);
            }
            if (flags & 2) {
                gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, this.glFilter[texture._magFilter]);
            }
            if (flags & 4) {
                if (this.webgl2) {
                    gl.texParameteri(target, gl.TEXTURE_WRAP_S, this.glAddress[texture._addressU]);
                } else {
                    // WebGL1 doesn't support all addressing modes with NPOT textures
                    gl.texParameteri(target, gl.TEXTURE_WRAP_S, this.glAddress[texture.pot ? texture._addressU : pc.ADDRESS_CLAMP_TO_EDGE]);
                }
            }
            if (flags & 8) {
                if (this.webgl2) {
                    gl.texParameteri(target, gl.TEXTURE_WRAP_T, this.glAddress[texture._addressV]);
                } else {
                    // WebGL1 doesn't support all addressing modes with NPOT textures
                    gl.texParameteri(target, gl.TEXTURE_WRAP_T, this.glAddress[texture.pot ? texture._addressV : pc.ADDRESS_CLAMP_TO_EDGE]);
                }
            }
            if (flags & 16) {
                if (this.webgl2) {
                    gl.texParameteri(target, gl.TEXTURE_WRAP_R, this.glAddress[texture._addressW]);
                }
            }
            if (flags & 32) {
                if (this.webgl2) {
                    gl.texParameteri(target, gl.TEXTURE_COMPARE_MODE, texture._compareOnRead ? gl.COMPARE_REF_TO_TEXTURE : gl.NONE);
                }
            }
            if (flags & 64) {
                if (this.webgl2) {
                    gl.texParameteri(target, gl.TEXTURE_COMPARE_FUNC, this.glComparison[texture._compareFunc]);
                }
            }
            if (flags & 128) {
                var ext = this.extTextureFilterAnisotropic;
                if (ext) {
                    gl.texParameterf(target, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.max(1, Math.min(Math.round(texture._anisotropy), this.maxAnisotropy)));
                }
            }
        },

        setTexture: function (texture, textureUnit) {
            if (!texture._glTexture)
                this.initializeTexture(texture);

            if (texture._parameterFlags > 0 || texture._needsUpload || texture._needsMipmapsUpload || texture === this.grabPassTexture) {
                // Ensure the specified texture unit is active
                this.activeTexture(textureUnit);
                // Ensure the texture is bound on correct target of the specified texture unit
                this.bindTexture(texture);

                if (texture._parameterFlags) {
                    this.setTextureParameters(texture);
                    texture._parameterFlags = 0;
                }

                if (texture === this.grabPassTexture) {
                    this.updateGrabPass();

                } else if (texture._needsUpload || texture._needsMipmapsUpload) {
                    this.uploadTexture(texture);
                    texture._needsUpload = false;
                    texture._needsMipmapsUpload = false;
                }
            } else {
                // Ensure the texture is currently bound to the correct target on the specified texture unit.
                // If the texture is already bound to the correct target on the specified unit, there's no need
                // to actually make the specified texture unit active because the texture itself does not need
                // to be udpated.
                this.bindTextureOnUnit(texture, textureUnit);
            }
        },

        setBuffers: function (numInstances) {
            var gl = this.gl;
            var attribute, element, vertexBuffer, vbOffset, bufferId, locationId;
            var attributes = this.shader.attributes;

            // Commit the vertex buffer inputs
            if (this.attributesInvalidated) {
                for (var i = 0, len = attributes.length; i < len; i++) {
                    attribute = attributes[i];

                    // Retrieve vertex element for this shader attribute
                    element = attribute.scopeId.value;

                    // Check the vertex element is valid
                    if (element !== null) {
                        // Retrieve the vertex buffer that contains this element
                        vertexBuffer = this.vertexBuffers[element.stream];
                        vbOffset = this.vbOffsets[element.stream] || 0;

                        // Set the active vertex buffer object
                        bufferId = vertexBuffer.bufferId;
                        if (this.boundBuffer !== bufferId) {
                            gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
                            this.boundBuffer = bufferId;
                        }

                        // Hook the vertex buffer to the shader program
                        locationId = attribute.locationId;
                        if (!this.enabledAttributes[locationId]) {
                            gl.enableVertexAttribArray(locationId);
                            this.enabledAttributes[locationId] = true;
                        }
                        gl.vertexAttribPointer(
                            locationId,
                            element.numComponents,
                            this.glType[element.dataType],
                            element.normalize,
                            element.stride,
                            element.offset + vbOffset
                        );

                        if (element.stream === 1 && numInstances > 1) {
                            if (!this.instancedAttribs[locationId]) {
                                gl.vertexAttribDivisor(locationId, 1);
                                this.instancedAttribs[locationId] = true;
                            }
                        } else if (this.instancedAttribs[locationId]) {
                            gl.vertexAttribDivisor(locationId, 0);
                            this.instancedAttribs[locationId] = false;
                        }
                    }
                }

                this.attributesInvalidated = false;
            }

            // Set the active index buffer object
            bufferId = this.indexBuffer ? this.indexBuffer.bufferId : null;
            if (this.boundElementBuffer !== bufferId) {
                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferId);
                this.boundElementBuffer = bufferId;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#draw
         * @description Submits a graphical primitive to the hardware for immediate rendering.
         * @param {Object} primitive Primitive object describing how to submit current vertex/index buffers defined as follows:
         * @param {Number} primitive.type The type of primitive to render. Can be:
         * <ul>
         *     <li>{@link pc.PRIMITIVE_POINTS}</li>
         *     <li>{@link pc.PRIMITIVE_LINES}</li>
         *     <li>{@link pc.PRIMITIVE_LINELOOP}</li>
         *     <li>{@link pc.PRIMITIVE_LINESTRIP}</li>
         *     <li>{@link pc.PRIMITIVE_TRIANGLES}</li>
         *     <li>{@link pc.PRIMITIVE_TRISTRIP}</li>
         *     <li>{@link pc.PRIMITIVE_TRIFAN}</li>
         * </ul>
         * @param {Number} primitive.base The offset of the first index or vertex to dispatch in the draw call.
         * @param {Number} primitive.count The number of indices or vertices to dispatch in the draw call.
         * @param {Boolean} [primitive.indexed] True to interpret the primitive as indexed, thereby using the currently set index buffer and false otherwise.
         * @param {Number} [numInstances=1] The number of instances to render when using ANGLE_instanced_arrays. Defaults to 1.
         * @example
         * // Render a single, unindexed triangle
         * device.draw({
         *     type: pc.PRIMITIVE_TRIANGLES,
         *     base: 0,
         *     count: 3,
         *     indexed: false
         * )};
         */
        draw: function (primitive, numInstances) {
            var gl = this.gl;

            var i, j, len; // Loop counting
            var sampler, samplerValue, texture, numTextures; // Samplers
            var uniform, scopeId, uniformVersion, programVersion; // Uniforms
            var shader = this.shader;
            var samplers = shader.samplers;
            var uniforms = shader.uniforms;

            if (numInstances > 1) {
                this.boundBuffer = null;
                this.attributesInvalidated = true;
            }

            this.setBuffers(numInstances);

            // Commit the shader program variables
            var textureUnit = 0;

            for (i = 0, len = samplers.length; i < len; i++) {
                sampler = samplers[i];
                samplerValue = sampler.scopeId.value;
                if (!samplerValue) {
                    continue; // Because unset constants shouldn't raise random errors
                }

                if (samplerValue instanceof pc.Texture) {
                    texture = samplerValue;
                    this.setTexture(texture, textureUnit);

                    // #ifdef DEBUG
                    if (this.renderTarget) {
                        // Set breakpoint here to debug "Source and destination textures of the draw are the same" errors
                        if (this.renderTarget._samples < 2) {
                            if (this.renderTarget.colorBuffer && this.renderTarget.colorBuffer === texture) {
                                console.error("Trying to bind current color buffer as a texture");
                            } else if (this.renderTarget.depthBuffer && this.renderTarget.depthBuffer === texture) {
                                console.error("Trying to bind current depth buffer as a texture");
                            }
                        }
                    }
                    // #endif

                    if (sampler.slot !== textureUnit) {
                        gl.uniform1i(sampler.locationId, textureUnit);
                        sampler.slot = textureUnit;
                    }
                    textureUnit++;
                } else { // Array
                    sampler.array.length = 0;
                    numTextures = samplerValue.length;
                    for (j = 0; j < numTextures; j++) {
                        texture = samplerValue[j];
                        this.setTexture(texture, textureUnit);

                        sampler.array[j] = textureUnit;
                        textureUnit++;
                    }
                    gl.uniform1iv(sampler.locationId, sampler.array);
                }
            }

            // Commit any updated uniforms
            for (i = 0, len = uniforms.length; i < len; i++) {
                uniform = uniforms[i];
                scopeId = uniform.scopeId;
                uniformVersion = uniform.version;
                programVersion = scopeId.versionObject.version;

                // Check the value is valid
                if (uniformVersion.globalId !== programVersion.globalId || uniformVersion.revision !== programVersion.revision) {
                    uniformVersion.globalId = programVersion.globalId;
                    uniformVersion.revision = programVersion.revision;

                    // Call the function to commit the uniform value
                    if (scopeId.value !== null) {
                        this.commitFunction[uniform.dataType](uniform, scopeId.value);
                    }
                }
            }

            if (this.webgl2 && this.transformFeedbackBuffer) {
                // Enable TF, start writing to out buffer
                gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.transformFeedbackBuffer.bufferId);
                gl.beginTransformFeedback(gl.POINTS);
            }

            var mode = this.glPrimitive[primitive.type];
            var count = primitive.count;

            if (primitive.indexed) {
                var indexBuffer = this.indexBuffer;
                var format = indexBuffer.glFormat;
                var offset = primitive.base * indexBuffer.bytesPerIndex;

                if (numInstances > 1) {
                    gl.drawElementsInstanced(mode, count, format, offset, numInstances);
                } else {
                    gl.drawElements(mode, count, format, offset);
                }
            } else {
                var first = primitive.base;

                if (numInstances > 1) {
                    gl.drawArraysInstanced(mode, first, count, numInstances);
                } else {
                    gl.drawArrays(mode, first, count);
                }
            }

            if (this.webgl2 && this.transformFeedbackBuffer) {
                // disable TF
                gl.endTransformFeedback();
                gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
            }

            // #ifdef PROFILER
            this._drawCallsPerFrame++;
            this._primsPerFrame[primitive.type] += primitive.count * (numInstances > 1 ? numInstances : 1);
            // #endif
        },

        /**
         * @function
         * @name pc.GraphicsDevice#clear
         * @description Clears the frame buffer of the currently set render target.
         * @param {Object} options Optional options object that controls the behavior of the clear operation defined as follows:
         * @param {Number[]} options.color The color to clear the color buffer to in the range 0.0 to 1.0 for each component.
         * @param {Number} options.depth The depth value to clear the depth buffer to in the range 0.0 to 1.0.
         * @param {Number} options.flags The buffers to clear (the types being color, depth and stencil). Can be any bitwise
         * combination of:
         * <ul>
         *     <li>pc.CLEARFLAG_COLOR</li>
         *     <li>pc.CLEARFLAG_DEPTH</li>
         *     <li>pc.CLEARFLAG_STENCIL</li>
         * </ul>
         * @example
         * // Clear color buffer to black and depth buffer to 1.0
         * device.clear();
         *
         * // Clear just the color buffer to red
         * device.clear({
         *     color: [1, 0, 0, 1],
         *     flags: pc.CLEARFLAG_COLOR
         * });
         *
         * // Clear color buffer to yellow and depth to 1.0
         * device.clear({
         *     color: [1, 1, 0, 1],
         *     depth: 1.0,
         *     flags: pc.CLEARFLAG_COLOR | pc.CLEARFLAG_DEPTH
         * });
         */
        clear: function (options) {
            var defaultOptions = this.defaultClearOptions;
            options = options || defaultOptions;

            var flags = (options.flags == undefined) ? defaultOptions.flags : options.flags;
            if (flags !== 0) {
                var gl = this.gl;

                // Set the clear color
                if (flags & pc.CLEARFLAG_COLOR) {
                    var color = (options.color == undefined) ? defaultOptions.color : options.color;
                    this.setClearColor(color[0], color[1], color[2], color[3]);
                }

                if (flags & pc.CLEARFLAG_DEPTH) {
                    // Set the clear depth
                    var depth = (options.depth == undefined) ? defaultOptions.depth : options.depth;
                    this.setClearDepth(depth);
                    if (!this.depthWrite) {
                        gl.depthMask(true);
                    }
                }

                if (flags & pc.CLEARFLAG_STENCIL) {
                    // Set the clear stencil
                    var stencil = (options.stencil == undefined) ? defaultOptions.stencil : options.stencil;
                    this.setClearStencil(stencil);
                }

                // Clear the frame buffer
                gl.clear(this.glClearFlag[flags]);

                if (flags & pc.CLEARFLAG_DEPTH) {
                    if (!this.depthWrite) {
                        gl.depthMask(false);
                    }
                }
            }
        },

        readPixels: function (x, y, w, h, pixels) {
            var gl = this.gl;
            gl.readPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
        },

        setClearDepth: function (depth) {
            if (depth !== this.clearDepth) {
                this.gl.clearDepth(depth);
                this.clearDepth = depth;
            }
        },

        setClearColor: function (r, g, b, a) {
            if ((r !== this.clearRed) || (g !== this.clearGreen) || (b !== this.clearBlue) || (a !== this.clearAlpha)) {
                this.gl.clearColor(r, g, b, a);
                this.clearRed = r;
                this.clearGreen = g;
                this.clearBlue = b;
                this.clearAlpha = a;
            }
        },

        setClearStencil: function (value) {
            if (value !== this.clearStencil) {
                this.gl.clearStencil(value);
                this.clearStencil = value;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setRenderTarget
         * @description Sets the specified render target on the device. If null
         * is passed as a parameter, the back buffer becomes the current target
         * for all rendering operations.
         * @param {pc.RenderTarget} renderTarget The render target to activate.
         * @example
         * // Set a render target to receive all rendering output
         * device.setRenderTarget(renderTarget);
         *
         * // Set the back buffer to receive all rendering output
         * device.setRenderTarget(null);
         */
        setRenderTarget: function (renderTarget) {
            this.renderTarget = renderTarget;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#getRenderTarget
         * @description Queries the currently set render target on the device.
         * @returns {pc.RenderTarget} The current render target.
         * @example
         * // Get the current render target
         * var renderTarget = device.getRenderTarget();
         */
        getRenderTarget: function () {
            return this.renderTarget;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#getDepthTest
         * @description Queries whether depth testing is enabled.
         * @returns {Boolean} true if depth testing is enabled and false otherwise.
         * @example
         * var depthTest = device.getDepthTest();
         * console.log('Depth testing is ' + depthTest ? 'enabled' : 'disabled');
         */
        getDepthTest: function () {
            return this.depthTest;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setDepthTest
         * @description Enables or disables depth testing of fragments. Once this state
         * is set, it persists until it is changed. By default, depth testing is enabled.
         * @param {Boolean} depthTest true to enable depth testing and false otherwise.
         * @example
         * device.setDepthTest(true);
         */
        setDepthTest: function (depthTest) {
            if (this.depthTest !== depthTest) {
                var gl = this.gl;
                if (depthTest) {
                    gl.enable(gl.DEPTH_TEST);
                } else {
                    gl.disable(gl.DEPTH_TEST);
                }
                this.depthTest = depthTest;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setDepthFunc
         * @description Configures the depth test.
         * @param {Number} func A function to compare a new depth value with an existing z-buffer value and decide if to write a pixel. Can be:
         * <ul>
         *     <li>pc.FUNC_NEVER: don't draw</li>
         *     <li>pc.FUNC_LESS: draw if new depth < depth buffer</li>
         *     <li>pc.FUNC_EQUAL: draw if new depth == depth buffer</li>
         *     <li>pc.FUNC_LESSEQUAL: draw if new depth <= depth buffer</li>
         *     <li>pc.FUNC_GREATER: draw if new depth > depth buffer</li>
         *     <li>pc.FUNC_NOTEQUAL: draw if new depth != depth buffer</li>
         *     <li>pc.FUNC_GREATEREQUAL: draw if new depth >= depth buffer</li>
         *     <li>pc.FUNC_ALWAYS: always draw</li>
         * </ul>
         */
        setDepthFunc: function (func) {
            if (this.depthFunc === func) return;
            this.gl.depthFunc(this.glComparison[func]);
            this.depthFunc = func;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#getDepthWrite
         * @description Queries whether writes to the depth buffer are enabled.
         * @returns {Boolean} true if depth writing is enabled and false otherwise.
         * @example
         * var depthWrite = device.getDepthWrite();
         * console.log('Depth writing is ' + depthWrite ? 'enabled' : 'disabled');
         */
        getDepthWrite: function () {
            return this.depthWrite;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setDepthWrite
         * @description Enables or disables writes to the depth buffer. Once this state
         * is set, it persists until it is changed. By default, depth writes are enabled.
         * @param {Boolean} writeDepth true to enable depth writing and false otherwise.
         * @example
         * device.setDepthWrite(true);
         */
        setDepthWrite: function (writeDepth) {
            if (this.depthWrite !== writeDepth) {
                this.gl.depthMask(writeDepth);
                this.depthWrite = writeDepth;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setColorWrite
         * @description Enables or disables writes to the color buffer. Once this state
         * is set, it persists until it is changed. By default, color writes are enabled
         * for all color channels.
         * @param {Boolean} writeRed true to enable writing of the red channel and false otherwise.
         * @param {Boolean} writeGreen true to enable writing of the green channel and false otherwise.
         * @param {Boolean} writeBlue true to enable writing of the blue channel and false otherwise.
         * @param {Boolean} writeAlpha true to enable writing of the alpha channel and false otherwise.
         * @example
         * // Just write alpha into the frame buffer
         * device.setColorWrite(false, false, false, true);
         */
        setColorWrite: function (writeRed, writeGreen, writeBlue, writeAlpha) {
            if ((this.writeRed !== writeRed) ||
                (this.writeGreen !== writeGreen) ||
                (this.writeBlue !== writeBlue) ||
                (this.writeAlpha !== writeAlpha)) {
                this.gl.colorMask(writeRed, writeGreen, writeBlue, writeAlpha);
                this.writeRed = writeRed;
                this.writeGreen = writeGreen;
                this.writeBlue = writeBlue;
                this.writeAlpha = writeAlpha;
            }
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#setAlphaToCoverage
         * @description Enables or disables alpha to coverage (WebGL2 only).
         * @param {Boolean} state True to enable alpha to coverage and false to disable it.
         */
        setAlphaToCoverage: function (state) {
            if (!this.webgl2) return;
            if (this.alphaToCoverage === state) return;
            this.alphaToCoverage = state;

            if (state) {
                this.gl.enable(this.gl.SAMPLE_ALPHA_TO_COVERAGE);
            } else {
                this.gl.disable(this.gl.SAMPLE_ALPHA_TO_COVERAGE);
            }
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#setTransformFeedbackBuffer
         * @description Sets the output vertex buffer. It will be written to by a shader with transform feedback varyings.
         * @param {pc.VertexBuffer} tf The output vertex buffer
         */
        setTransformFeedbackBuffer: function (tf) {
            if (this.transformFeedbackBuffer === tf)
                return;

            this.transformFeedbackBuffer = tf;

            if (this.webgl2) {
                var gl = this.gl;
                if (tf) {
                    if (!this.feedback) {
                        this.feedback = gl.createTransformFeedback();
                    }
                    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.feedback);
                } else {
                    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
                }
            }
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#setRaster
         * @description Enables or disables rasterization. Useful with transform feedback, when you only need to process the data without drawing.
         * @param {Boolean} on True to enable rasterization and false to disable it.
         */
        setRaster: function (on) {
            if (this.raster === on) return;

            this.raster = on;

            if (this.webgl2) {
                if (on) {
                    this.gl.disable(this.gl.RASTERIZER_DISCARD);
                } else {
                    this.gl.enable(this.gl.RASTERIZER_DISCARD);
                }
            }
        },

        setDepthBias: function (on) {
            if (this.depthBiasEnabled === on) return;

            this.depthBiasEnabled = on;

            if (on) {
                this.gl.enable(this.gl.POLYGON_OFFSET_FILL);
            } else {
                this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
            }
        },

        setDepthBiasValues: function (constBias, slopeBias) {
            this.gl.polygonOffset(slopeBias, constBias);
        },

        /**
         * @function
         * @name pc.GraphicsDevice#getBlending
         * @description Queries whether blending is enabled.
         * @returns {Boolean} True if blending is enabled and false otherwise.
         */
        getBlending: function () {
            return this.blending;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setBlending
         * @description Enables or disables blending.
         * @param {Boolean} blending True to enable blending and false to disable it.
         */
        setBlending: function (blending) {
            if (this.blending !== blending) {
                var gl = this.gl;
                if (blending) {
                    gl.enable(gl.BLEND);
                } else {
                    gl.disable(gl.BLEND);
                }
                this.blending = blending;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilTest
         * @description Enables or disables stencil test.
         * @param {Boolean} enable True to enable stencil test and false to disable it.
         */
        setStencilTest: function (enable) {
            if (this.stencil !== enable) {
                var gl = this.gl;
                if (enable) {
                    gl.enable(gl.STENCIL_TEST);
                } else {
                    gl.disable(gl.STENCIL_TEST);
                }
                this.stencil = enable;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilFunc
         * @description Configures stencil test for both front and back faces.
         * @param {Number} func A comparison function that decides if the pixel should be written, based on the current stencil buffer value,
         * reference value, and mask value. Can be:
         * <ul>
         *     <li>pc.FUNC_NEVER: never pass</li>
         *     <li>pc.FUNC_LESS: pass if (ref & mask) < (stencil & mask)</li>
         *     <li>pc.FUNC_EQUAL: pass if (ref & mask) == (stencil & mask)</li>
         *     <li>pc.FUNC_LESSEQUAL: pass if (ref & mask) <= (stencil & mask)</li>
         *     <li>pc.FUNC_GREATER: pass if (ref & mask) > (stencil & mask)</li>
         *     <li>pc.FUNC_NOTEQUAL: pass if (ref & mask) != (stencil & mask)</li>
         *     <li>pc.FUNC_GREATEREQUAL: pass if (ref & mask) >= (stencil & mask)</li>
         *     <li>pc.FUNC_ALWAYS: always pass</li>
         * </ul>
         * @param {Number} ref Reference value used in comparison.
         * @param {Number} mask Mask applied to stencil buffer value and reference value before comparison.
         */
        setStencilFunc: function (func, ref, mask) {
            if (this.stencilFuncFront !== func || this.stencilRefFront !== ref || this.stencilMaskFront !== mask ||
                this.stencilFuncBack !== func || this.stencilRefBack !== ref || this.stencilMaskBack !== mask) {
                var gl = this.gl;
                gl.stencilFunc(this.glComparison[func], ref, mask);
                this.stencilFuncFront = this.stencilFuncBack = func;
                this.stencilRefFront = this.stencilRefBack = ref;
                this.stencilMaskFront = this.stencilMaskBack = mask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilFuncFront
         * @description Configures stencil test for front faces.
         * @param {Number} func A comparison function that decides if the pixel should be written,
         * based on the current stencil buffer value, reference value, and mask value. Can be:
         * <ul>
         *     <li>pc.FUNC_NEVER: never pass</li>
         *     <li>pc.FUNC_LESS: pass if (ref & mask) < (stencil & mask)</li>
         *     <li>pc.FUNC_EQUAL: pass if (ref & mask) == (stencil & mask)</li>
         *     <li>pc.FUNC_LESSEQUAL: pass if (ref & mask) <= (stencil & mask)</li>
         *     <li>pc.FUNC_GREATER: pass if (ref & mask) > (stencil & mask)</li>
         *     <li>pc.FUNC_NOTEQUAL: pass if (ref & mask) != (stencil & mask)</li>
         *     <li>pc.FUNC_GREATEREQUAL: pass if (ref & mask) >= (stencil & mask)</li>
         *     <li>pc.FUNC_ALWAYS: always pass</li>
         * </ul>
         * @param {Number} ref Reference value used in comparison.
         * @param {Number} mask Mask applied to stencil buffer value and reference value before comparison.
         */
        setStencilFuncFront: function (func, ref, mask) {
            if (this.stencilFuncFront !== func || this.stencilRefFront !== ref || this.stencilMaskFront !== mask) {
                var gl = this.gl;
                gl.stencilFuncSeparate(gl.FRONT, this.glComparison[func], ref, mask);
                this.stencilFuncFront = func;
                this.stencilRefFront = ref;
                this.stencilMaskFront = mask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilFuncBack
         * @description Configures stencil test for back faces.
         * @param {Number} func A comparison function that decides if the pixel should be written,
         * based on the current stencil buffer value, reference value, and mask value. Can be:
         * <ul>
         *     <li>pc.FUNC_NEVER: never pass</li>
         *     <li>pc.FUNC_LESS: pass if (ref & mask) < (stencil & mask)</li>
         *     <li>pc.FUNC_EQUAL: pass if (ref & mask) == (stencil & mask)</li>
         *     <li>pc.FUNC_LESSEQUAL: pass if (ref & mask) <= (stencil & mask)</li>
         *     <li>pc.FUNC_GREATER: pass if (ref & mask) > (stencil & mask)</li>
         *     <li>pc.FUNC_NOTEQUAL: pass if (ref & mask) != (stencil & mask)</li>
         *     <li>pc.FUNC_GREATEREQUAL: pass if (ref & mask) >= (stencil & mask)</li>
         *     <li>pc.FUNC_ALWAYS: always pass</li>
         * </ul>
         * @param {Number} ref Reference value used in comparison.
         * @param {Number} mask Mask applied to stencil buffer value and reference value before comparison.
         */
        setStencilFuncBack: function (func, ref, mask) {
            if (this.stencilFuncBack !== func || this.stencilRefBack !== ref || this.stencilMaskBack !== mask) {
                var gl = this.gl;
                gl.stencilFuncSeparate(gl.BACK, this.glComparison[func], ref, mask);
                this.stencilFuncBack = func;
                this.stencilRefBack = ref;
                this.stencilMaskBack = mask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilOperation
         * @description Configures how stencil buffer values should be modified based on the result
         * of depth/stencil tests. Works for both front and back faces.
         * @param {Number} fail Action to take if stencil test is failed
         * @param {Number} zfail Action to take if depth test is failed
         * @param {Number} zpass Action to take if both depth and stencil test are passed
         * All arguments can be:
         * <ul>
         *     <li>pc.STENCILOP_KEEP: don't change the stencil buffer value</li>
         *     <li>pc.STENCILOP_ZERO: set value to zero</li>
         *     <li>pc.STENCILOP_REPLACE: replace value with the reference value (see {@link pc.GraphicsDevice#setStencilFunc})</li>
         *     <li>pc.STENCILOP_INCREMENT: increment the value</li>
         *     <li>pc.STENCILOP_INCREMENTWRAP: increment the value, but wrap it to zero when it's larger than a maximum representable value</li>
         *     <li>pc.STENCILOP_DECREMENT: decrement the value</li>
         *     <li>pc.STENCILOP_DECREMENTWRAP: decrement the value, but wrap it to a maximum representable value, if the current value is 0</li>
         *     <li>pc.STENCILOP_INVERT: invert the value bitwise</li>
         * </ul>
         * @param {Number} writeMask A bit mask applied to the reference value, when written.
         */
        setStencilOperation: function (fail, zfail, zpass, writeMask) {
            if (this.stencilFailFront !== fail || this.stencilZfailFront !== zfail || this.stencilZpassFront !== zpass ||
                this.stencilFailBack !== fail || this.stencilZfailBack !== zfail || this.stencilZpassBack !== zpass) {
                this.gl.stencilOp(this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]);
                this.stencilFailFront = this.stencilFailBack = fail;
                this.stencilZfailFront = this.stencilZfailBack = zfail;
                this.stencilZpassFront = this.stencilZpassBack = zpass;
            }
            if (this.stencilWriteMaskFront !== writeMask || this.stencilWriteMaskBack !== writeMask) {
                this.gl.stencilMask(writeMask);
                this.stencilWriteMaskFront = writeMask;
                this.stencilWriteMaskBack = writeMask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilOperationFront
         * @description Configures how stencil buffer values should be modified based on the result
         * of depth/stencil tests. Works for front faces.
         * @param {Number} fail Action to take if stencil test is failed
         * @param {Number} zfail Action to take if depth test is failed
         * @param {Number} zpass Action to take if both depth and stencil test are passed
         * All arguments can be:
         * <ul>
         *     <li>pc.STENCILOP_KEEP: don't change the stencil buffer value</li>
         *     <li>pc.STENCILOP_ZERO: set value to zero</li>
         *     <li>pc.STENCILOP_REPLACE: replace value with the reference value (see {@link pc.GraphicsDevice#setStencilFunc})</li>
         *     <li>pc.STENCILOP_INCREMENT: increment the value</li>
         *     <li>pc.STENCILOP_INCREMENTWRAP: increment the value, but wrap it to zero when it's larger than a maximum representable value</li>
         *     <li>pc.STENCILOP_DECREMENT: decrement the value</li>
         *     <li>pc.STENCILOP_DECREMENTWRAP: decrement the value, but wrap it to a maximum representable value, if the current value is 0</li>
         *     <li>pc.STENCILOP_INVERT: invert the value bitwise</li>
         * </ul>
         * @param {Number} writeMask A bit mask applied to the reference value, when written.
         */
        setStencilOperationFront: function (fail, zfail, zpass, writeMask) {
            if (this.stencilFailFront !== fail || this.stencilZfailFront !== zfail || this.stencilZpassFront !== zpass) {
                this.gl.stencilOpSeparate(this.gl.FRONT, this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]);
                this.stencilFailFront = fail;
                this.stencilZfailFront = zfail;
                this.stencilZpassFront = zpass;
            }
            if (this.stencilWriteMaskFront !== writeMask) {
                this.gl.stencilMaskSeparate(this.gl.FRONT, writeMask);
                this.stencilWriteMaskFront = writeMask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setStencilOperationBack
         * @description Configures how stencil buffer values should be modified based on the result
         * of depth/stencil tests. Works for back faces.
         * @param {Number} fail Action to take if stencil test is failed
         * @param {Number} zfail Action to take if depth test is failed
         * @param {Number} zpass Action to take if both depth and stencil test are passed
         * All arguments can be:
         * <ul>
         *     <li>pc.STENCILOP_KEEP: don't change the stencil buffer value</li>
         *     <li>pc.STENCILOP_ZERO: set value to zero</li>
         *     <li>pc.STENCILOP_REPLACE: replace value with the reference value (see {@link pc.GraphicsDevice#setStencilFunc})</li>
         *     <li>pc.STENCILOP_INCREMENT: increment the value</li>
         *     <li>pc.STENCILOP_INCREMENTWRAP: increment the value, but wrap it to zero when it's larger than a maximum representable value</li>
         *     <li>pc.STENCILOP_DECREMENT: decrement the value</li>
         *     <li>pc.STENCILOP_DECREMENTWRAP: decrement the value, but wrap it to a maximum representable value, if the current value is 0</li>
         *     <li>pc.STENCILOP_INVERT: invert the value bitwise</li>
         * </ul>
         * @param {Number} writeMask A bit mask applied to the reference value, when written.
         */
        setStencilOperationBack: function (fail, zfail, zpass, writeMask) {
            if (this.stencilFailBack !== fail || this.stencilZfailBack !== zfail || this.stencilZpassBack !== zpass) {
                this.gl.stencilOpSeparate(this.gl.BACK, this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]);
                this.stencilFailBack = fail;
                this.stencilZfailBack = zfail;
                this.stencilZpassBack = zpass;
            }
            if (this.stencilWriteMaskBack !== writeMask) {
                this.gl.stencilMaskSeparate(this.gl.BACK, writeMask);
                this.stencilWriteMaskBack = writeMask;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setBlendFunction
         * @description Configures blending operations. Both source and destination
         * blend modes can take the following values:
         * <ul>
         *     <li>pc.BLENDMODE_ZERO</li>
         *     <li>pc.BLENDMODE_ONE</li>
         *     <li>pc.BLENDMODE_SRC_COLOR</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_SRC_COLOR</li>
         *     <li>pc.BLENDMODE_DST_COLOR</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_DST_COLOR</li>
         *     <li>pc.BLENDMODE_SRC_ALPHA</li>
         *     <li>pc.BLENDMODE_SRC_ALPHA_SATURATE</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_SRC_ALPHA</li>
         *     <li>pc.BLENDMODE_DST_ALPHA</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_DST_ALPHA</li>
         * </ul>
         * @param {Number} blendSrc The source blend function.
         * @param {Number} blendDst The destination blend function.
         */
        setBlendFunction: function (blendSrc, blendDst) {
            if (this.blendSrc !== blendSrc || this.blendDst !== blendDst || this.separateAlphaBlend) {
                this.gl.blendFunc(this.glBlendFunction[blendSrc], this.glBlendFunction[blendDst]);
                this.blendSrc = blendSrc;
                this.blendDst = blendDst;
                this.separateAlphaBlend = false;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setBlendFunctionSeparate
         * @description Configures blending operations. Both source and destination
         * blend modes can take the following values:
         * <ul>
         *     <li>pc.BLENDMODE_ZERO</li>
         *     <li>pc.BLENDMODE_ONE</li>
         *     <li>pc.BLENDMODE_SRC_COLOR</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_SRC_COLOR</li>
         *     <li>pc.BLENDMODE_DST_COLOR</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_DST_COLOR</li>
         *     <li>pc.BLENDMODE_SRC_ALPHA</li>
         *     <li>pc.BLENDMODE_SRC_ALPHA_SATURATE</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_SRC_ALPHA</li>
         *     <li>pc.BLENDMODE_DST_ALPHA</li>
         *     <li>pc.BLENDMODE_ONE_MINUS_DST_ALPHA</li>
         * </ul>
         * @param {Number} blendSrc The source blend function.
         * @param {Number} blendDst The destination blend function.
         * @param {Number} blendSrcAlpha The separate source blend function for the alpha channel.
         * @param {Number} blendDstAlpha The separate destination blend function for the alpha channel.
         */
        setBlendFunctionSeparate: function (blendSrc, blendDst, blendSrcAlpha, blendDstAlpha) {
            if (this.blendSrc !== blendSrc || this.blendDst !== blendDst || this.blendSrcAlpha !== blendSrcAlpha || this.blendDstAlpha !== blendDstAlpha || !this.separateAlphaBlend) {
                this.gl.blendFuncSeparate(this.glBlendFunction[blendSrc], this.glBlendFunction[blendDst],
                                          this.glBlendFunction[blendSrcAlpha], this.glBlendFunction[blendDstAlpha]);
                this.blendSrc = blendSrc;
                this.blendDst = blendDst;
                this.blendSrcAlpha = blendSrcAlpha;
                this.blendDstAlpha = blendDstAlpha;
                this.separateAlphaBlend = true;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setBlendEquation
         * @description Configures the blending equation. The default blend equation is
         * pc.BLENDEQUATION_ADD.
         * @param {Number} blendEquation The blend equation. Can be:
         * <ul>
         *     <li>pc.BLENDEQUATION_ADD</li>
         *     <li>pc.BLENDEQUATION_SUBTRACT</li>
         *     <li>pc.BLENDEQUATION_REVERSE_SUBTRACT</li>
         *     <li>pc.BLENDEQUATION_MIN</li>
         *     <li>pc.BLENDEQUATION_MAX</li>
         * Note that MIN and MAX modes require either EXT_blend_minmax or WebGL2 to work (check device.extBlendMinmax).
         * </ul>
         */
        setBlendEquation: function (blendEquation) {
            if (this.blendEquation !== blendEquation || this.separateAlphaEquation) {
                this.gl.blendEquation(this.glBlendEquation[blendEquation]);
                this.blendEquation = blendEquation;
                this.separateAlphaEquation = false;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setBlendEquationSeparate
         * @description Configures the blending equation. The default blend equation is
         * pc.BLENDEQUATION_ADD.
         * @param {Number} blendEquation The blend equation. Can be:
         * <ul>
         *     <li>pc.BLENDEQUATION_ADD</li>
         *     <li>pc.BLENDEQUATION_SUBTRACT</li>
         *     <li>pc.BLENDEQUATION_REVERSE_SUBTRACT</li>
         *     <li>pc.BLENDEQUATION_MIN</li>
         *     <li>pc.BLENDEQUATION_MAX</li>
         * Note that MIN and MAX modes require either EXT_blend_minmax or WebGL2 to work (check device.extBlendMinmax).
         * @param {Number} blendAlphaEquation A separate blend equation for the alpha channel. Accepts same values as blendEquation.
         * </ul>
         */
        setBlendEquationSeparate: function (blendEquation, blendAlphaEquation) {
            if (this.blendEquation !== blendEquation || this.blendAlphaEquation !== blendAlphaEquation || !this.separateAlphaEquation) {
                this.gl.blendEquationSeparate(this.glBlendEquation[blendEquation], this.glBlendEquation[blendAlphaEquation]);
                this.blendEquation = blendEquation;
                this.blendAlphaEquation = blendAlphaEquation;
                this.separateAlphaEquation = true;
            }
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setCullMode
         * @description Controls how triangles are culled based on their face direction.
         * The default cull mode is pc.CULLFACE_BACK.
         * @param {Number} cullMode The cull mode to set. Can be:
         * <ul>
         *     <li>pc.CULLFACE_NONE</li>
         *     <li>pc.CULLFACE_BACK</li>
         *     <li>pc.CULLFACE_FRONT</li>
         *     <li>pc.CULLFACE_FRONTANDBACK</li>
         * </ul>
         */
        setCullMode: function (cullMode) {
            if (this.cullMode !== cullMode) {
                if (cullMode === pc.CULLFACE_NONE) {
                    this.gl.disable(this.gl.CULL_FACE);
                } else {
                    if (this.cullMode === pc.CULLFACE_NONE) {
                        this.gl.enable(this.gl.CULL_FACE);
                    }

                    var mode = this.glCull[cullMode];
                    if (this.cullFace !== mode) {
                        this.gl.cullFace(mode);
                        this.cullFace = mode;
                    }
                }
                this.cullMode = cullMode;
            }
        },

        getCullMode: function () {
            return this.cullMode;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setIndexBuffer
         * @description Sets the current index buffer on the graphics device. On subsequent
         * calls to pc.GraphicsDevice#draw, the specified index buffer will be used to provide
         * index data for any indexed primitives.
         * @param {pc.IndexBuffer} indexBuffer The index buffer to assign to the device.
         */
        setIndexBuffer: function (indexBuffer) {
            // Store the index buffer
            this.indexBuffer = indexBuffer;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setVertexBuffer
         * @description Sets the current vertex buffer for a specific stream index on the graphics
         * device. On subsequent calls to pc.GraphicsDevice#draw, the specified vertex buffer will be
         * used to provide vertex data for any primitives.
         * @param {pc.VertexBuffer} vertexBuffer The vertex buffer to assign to the device.
         * @param {Number} stream The stream index for the vertex buffer, indexed from 0 upwards.
         * @param {Number} [vbOffset=0] The byte offset into the vertex buffer data. Defaults to 0.
         */
        setVertexBuffer: function (vertexBuffer, stream, vbOffset) {
            if (this.vertexBuffers[stream] !== vertexBuffer || this.vbOffsets[stream] !== vbOffset) {
                // Store the vertex buffer for this stream index
                this.vertexBuffers[stream] = vertexBuffer;
                this.vbOffsets[stream] = vbOffset;

                // Push each vertex element in scope
                var vertexFormat = vertexBuffer.getFormat();
                var i = 0;
                var elements = vertexFormat.elements;
                var numElements = elements.length;
                while (i < numElements) {
                    var vertexElement = elements[i++];
                    vertexElement.stream = stream;
                    vertexElement.scopeId.setValue(vertexElement);
                }

                this.attributesInvalidated = true;
            }
        },

        compileShaderSource: function (src, isVertexShader) {
            var gl = this.gl;

            var glShader = isVertexShader ? this.vertexShaderCache[src] : this.fragmentShaderCache[src];

            if (!glShader) {
                // #ifdef PROFILER
                var startTime = pc.now();
                this.fire('shader:compile:start', {
                    timestamp: startTime,
                    target: this
                });
                // #endif

                glShader = gl.createShader(isVertexShader ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);

                gl.shaderSource(glShader, src);
                gl.compileShader(glShader);

                // #ifdef PROFILER
                var endTime = pc.now();
                this.fire('shader:compile:end', {
                    timestamp: endTime,
                    target: this
                });
                this._shaderStats.compileTime += endTime - startTime;
                // #endif

                if (isVertexShader) {
                    this.vertexShaderCache[src] = glShader;
                    // #ifdef PROFILER
                    this._shaderStats.vsCompiled++;
                    // #endif
                } else {
                    this.fragmentShaderCache[src] = glShader;
                    // #ifdef PROFILER
                    this._shaderStats.fsCompiled++;
                    // #endif
                }
            }

            return glShader;
        },

        compileAndLinkShader: function (shader) {
            var gl = this.gl;

            var definition = shader.definition;
            var glVertexShader = this.compileShaderSource(definition.vshader, true);
            var glFragmentShader = this.compileShaderSource(definition.fshader, false);

            var glProgram = gl.createProgram();

            gl.attachShader(glProgram, glVertexShader);
            gl.attachShader(glProgram, glFragmentShader);

            if (this.webgl2 && definition.useTransformFeedback) {
                // Collect all "out_" attributes and use them for output
                var attrs = definition.attributes;
                var outNames = [];
                for (var attr in attrs) {
                    if (attrs.hasOwnProperty(attr)) {
                        outNames.push("out_" + attr);
                    }
                }
                gl.transformFeedbackVaryings(glProgram, outNames, gl.INTERLEAVED_ATTRIBS);
            }

            gl.linkProgram(glProgram);

            // Cache the WebGL objects on the shader
            shader._glVertexShader = glVertexShader;
            shader._glFragmentShader = glFragmentShader;
            shader._glProgram = glProgram;

            // #ifdef PROFILER
            this._shaderStats.linked++;
            if (definition.tag === pc.SHADERTAG_MATERIAL) {
                this._shaderStats.materialShaders++;
            }
            // #endif
        },

        createShader: function (shader) {
            this.compileAndLinkShader(shader);

            this.shaders.push(shader);
        },

        destroyShader: function (shader) {
            var idx = this.shaders.indexOf(shader);
            if (idx !== -1) {
                this.shaders.splice(idx, 1);
            }

            if (shader._glProgram) {
                this.gl.deleteProgram(shader._glProgram);
                shader._glProgram = null;
                this.removeShaderFromCache(shader);
            }
        },

        _addLineNumbers: function (src) {
            var lines = src.split("\n");

            // Chrome reports shader errors on lines indexed from 1
            for (var i = 0, len = lines.length; i < len; i++) {
                lines[i] = (i + 1) + ":\t" + lines[i];
            }

            return lines.join( "\n" );
        },

        postLink: function (shader) {
            var gl = this.gl;

            var glVertexShader = shader._glVertexShader;
            var glFragmentShader = shader._glFragmentShader;
            var glProgram = shader._glProgram;

            var definition = shader.definition;

            // #ifdef PROFILER
            var startTime = pc.now();
            this.fire('shader:link:start', {
                timestamp: startTime,
                target: this
            });
            // #endif

            // Check for errors
            if (!gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS)) {
                console.error("Failed to compile vertex shader:\n\n" + this._addLineNumbers(definition.vshader) + "\n\n" + gl.getShaderInfoLog(glVertexShader));
                return false;
            }
            if (!gl.getShaderParameter(glFragmentShader, gl.COMPILE_STATUS)) {
                console.error("Failed to compile fragment shader:\n\n" + this._addLineNumbers(definition.fshader) + "\n\n" + gl.getShaderInfoLog(glFragmentShader));
                return false;
            }
            if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
                console.error("Failed to link shader program. Error: " + gl.getProgramInfoLog(glProgram));
                return false;
            }

            var i, info, location, shaderInput;

            // Query the program for each vertex buffer input (GLSL 'attribute')
            i = 0;
            var numAttributes = gl.getProgramParameter(glProgram, gl.ACTIVE_ATTRIBUTES);
            while (i < numAttributes) {
                info = gl.getActiveAttrib(glProgram, i++);
                location = gl.getAttribLocation(glProgram, info.name);

                // Check attributes are correctly linked up
                if (definition.attributes[info.name] === undefined) {
                    console.error('Vertex shader attribute "' + info.name + '" is not mapped to a semantic in shader definition.');
                }

                shaderInput = new pc.ShaderInput(this, definition.attributes[info.name], this.pcUniformType[info.type], location);

                shader.attributes.push(shaderInput);
            }

            // Query the program for each shader state (GLSL 'uniform')
            i = 0;
            var numUniforms = gl.getProgramParameter(glProgram, gl.ACTIVE_UNIFORMS);
            while (i < numUniforms) {
                info = gl.getActiveUniform(glProgram, i++);
                location = gl.getUniformLocation(glProgram, info.name);

                shaderInput = new pc.ShaderInput(this, info.name, this.pcUniformType[info.type], location);

                if (info.type === gl.SAMPLER_2D || info.type === gl.SAMPLER_CUBE ||
                    (this.webgl2 && (info.type === gl.SAMPLER_2D_SHADOW || info.type === gl.SAMPLER_CUBE_SHADOW || info.type === gl.SAMPLER_3D))
                ) {
                    shader.samplers.push(shaderInput);
                } else {
                    shader.uniforms.push(shaderInput);
                }
            }

            shader.ready = true;

            // #ifdef PROFILER
            var endTime = pc.now();
            this.fire('shader:link:end', {
                timestamp: endTime,
                target: this
            });
            this._shaderStats.compileTime += endTime - startTime;
            // #endif

            return true;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#setShader
         * @description Sets the active shader to be used during subsequent draw calls.
         * @param {pc.Shader} shader The shader to set to assign to the device.
         * @returns {Boolean} true if the shader was successfully set, false otherwise.
         */
        setShader: function (shader) {
            if (shader !== this.shader) {
                if (!shader.ready) {
                    if (!this.postLink(shader)) {
                        return false;
                    }
                }

                this.shader = shader;

                // Set the active shader
                this.gl.useProgram(shader._glProgram);

                // #ifdef PROFILER
                this._shaderSwitchesPerFrame++;
                // #endif

                this.attributesInvalidated = true;
            }
            return true;
        },

        getHdrFormat: function () {
            if (this.textureHalfFloatRenderable) {
                return pc.PIXELFORMAT_RGB16F;
            } else if (this.textureFloatRenderable) {
                return pc.PIXELFORMAT_RGB32F;
            }
            return pc.PIXELFORMAT_R8_G8_B8_A8;
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#getBoneLimit
         * @description Queries the maximum number of bones that can be referenced by a shader.
         * The shader generators (pc.programlib) use this number to specify the matrix array
         * size of the uniform 'matrix_pose[0]'. The value is calculated based on the number of
         * available uniform vectors available after subtracting the number taken by a typical
         * heavyweight shader. If a different number is required, it can be tuned via
         * pc.GraphicsDevice#setBoneLimit.
         * @returns {Number} The maximum number of bones that can be supported by the host hardware.
         */
        getBoneLimit: function () {
            return this.boneLimit;
        },

        /**
         * @private
         * @function
         * @name pc.GraphicsDevice#setBoneLimit
         * @description Specifies the maximum number of bones that the device can support on
         * the current hardware. This function allows the default calculated value based on
         * available vector uniforms to be overridden.
         * @param {Number} maxBones The maximum number of bones supported by the host hardware.
         */
        setBoneLimit: function (maxBones) {
            this.boneLimit = maxBones;
        },

        /**
         * @function
         * @name pc.GraphicsDevice#resizeCanvas
         * @description Sets the width and height of the canvas, then fires the 'resizecanvas' event.
         * Note that the specified width and height values will be multiplied by the value of
         * {@link pc.GraphicsDevice#maxPixelRatio} to give the final resultant width and height for
         * the canvas.
         * @param {Number} width The new width of the canvas.
         * @param {Number} height The new height of the canvas.
         */
        resizeCanvas: function (width, height) {
            this._width = width;
            this._height = height;

            var ratio = Math.min(this._maxPixelRatio, window.devicePixelRatio);
            width *= ratio;
            height *= ratio;
            this.canvas.width = width;
            this.canvas.height = height;
            this.fire(EVENT_RESIZE, width, height);
        },

        setResolution: function (width, height) {
            this._width = width;
            this._height = height;
            this.canvas.width = width;
            this.canvas.height = height;
            this.fire(EVENT_RESIZE, width, height);
        },

        /**
         * @function
         * @name pc.GraphicsDevice#clearShaderCache
         * @description Frees memory from all shaders ever allocated with this device
         */
        clearShaderCache: function () {
            var gl = this.gl;
            var shaderSrc;
            for (shaderSrc in this.fragmentShaderCache) {
                gl.deleteShader(this.fragmentShaderCache[shaderSrc]);
                delete this.fragmentShaderCache[shaderSrc];
            }
            for (shaderSrc in this.vertexShaderCache) {
                gl.deleteShader(this.vertexShaderCache[shaderSrc]);
                delete this.vertexShaderCache[shaderSrc];
            }

            this.programLib.clearCache();
        },

        removeShaderFromCache: function (shader) {
            this.programLib.removeFromCache(shader);
        },

        destroy: function () {
            var gl = this.gl;

            this.destroyGrabPass();

            if (this.webgl2 && this.feedback) {
                gl.deleteTransformFeedback(this.feedback);
            }

            this.clearShaderCache();

            this.canvas.removeEventListener('webglcontextlost', this._contextLostHandler, false);
            this.canvas.removeEventListener('webglcontextrestored', this._contextRestoredHandler, false);

            this._contextLostHandler = null;
            this._contextRestoredHandler = null;

            this.canvas = null;
            this.gl = null;
        }
    });

    /**
     * @readonly
     * @name pc.GraphicsDevice#width
     * @type {Number}
     * @description Width of the back buffer in pixels.
     */
    Object.defineProperty(GraphicsDevice.prototype, 'width', {
        get: function () {
            return this.gl.drawingBufferWidth || this.canvas.width;
        }
    });

    /**
     * @readonly
     * @name pc.GraphicsDevice#height
     * @type {Number}
     * @description Height of the back buffer in pixels.
     */
    Object.defineProperty(GraphicsDevice.prototype, 'height', {
        get: function () {
            return this.gl.drawingBufferHeight || this.canvas.height;
        }
    });

    /**
     * @name pc.GraphicsDevice#fullscreen
     * @type {Boolean}
     * @description Fullscreen mode
     */
    Object.defineProperty(GraphicsDevice.prototype, 'fullscreen', {
        get: function () {
            return !!document.fullscreenElement;
        },
        set: function (fullscreen) {
            if (fullscreen) {
                var canvas = this.gl.canvas;
                canvas.requestFullscreen();
            } else {
                document.exitFullscreen();
            }
        }
    });

    /**
     * @private
     * @name pc.GraphicsDevice#enableAutoInstancing
     * @type {Boolean}
     * @description Automatic instancing
     */
    Object.defineProperty(GraphicsDevice.prototype, 'enableAutoInstancing', {
        get: function () {
            return this._enableAutoInstancing;
        },
        set: function (value) {
            this._enableAutoInstancing = value && this.extInstancing;
        }
    });

    /**
     * @name pc.GraphicsDevice#maxPixelRatio
     * @type {Number}
     * @description Maximum pixel ratio
     */
    Object.defineProperty(GraphicsDevice.prototype, 'maxPixelRatio', {
        get: function () {
            return this._maxPixelRatio;
        },
        set: function (ratio) {
            this._maxPixelRatio = ratio;
            this.resizeCanvas(this._width, this._height);
        }
    });

    /**
     * @readonly
     * @name pc.GraphicsDevice#textureFloatHighPrecision
     * @type {Number}
     * @description Check if high precision floating-point textures are supported
     */
    Object.defineProperty(GraphicsDevice.prototype, 'textureFloatHighPrecision', {
        get: function () {
            if (this._textureFloatHighPrecision === undefined) {
                this._textureFloatHighPrecision = testTextureFloatHighPrecision(this);
            }
            return this._textureFloatHighPrecision;
        }
    });

    return {
        GraphicsDevice: GraphicsDevice
    };
}());
